Skip to content

Commit e2785c1

Browse files
feat(pnpm): support pnpm as package manager (#2631)
* feat(pnpm): support pnpm as package manager * feat: support-pnpm-package-manager [ZEND-6808] Based on [this PR](#2631) , with minor tweaks. * Import node APIs in utils.spec.ts --------- Co-authored-by: michaelpineirocontentful <michael.pineiro@contentful.com>
1 parent 4a054c7 commit e2785c1

File tree

8 files changed

+419
-35
lines changed

8 files changed

+419
-35
lines changed

packages/contentful--create-contentful-app/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ npx create-contentful-app <app-name>
3030
# npm
3131
npm init contentful-app <app-name>
3232

33+
# pnpm
34+
pnpm init contentful-app <app-name>
35+
3336
# Yarn
3437
yarn create contentful-app <app-name>
3538
```
@@ -38,11 +41,9 @@ yarn create contentful-app <app-name>
3841

3942
### Package Manager
4043

41-
`--npm` or `--yarn`
42-
43-
Use npm or Yarn to manage dependencies. If omitted, defaults to the manager used to run `create-contentful-app`.
44+
`--npm` or `--pnpm` or `--yarn`
4445

45-
Both flags are mutually exclusive.
46+
Use npm, pnpm, or Yarn to manage dependencies. If omitted, or if more than one flag is passed, will default to the manager used to run `create-contentful-app`.
4647

4748
### Template
4849

packages/contentful--create-contentful-app/src/analytics.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Analytics } from '@segment/analytics-node';
2+
import type { PackageManager } from './types';
23

34
// Public write key scoped to data source
45
const SEGMENT_WRITE_KEY = 'IzCq3j4dQlTAgLdMykRW9oBHQKUy1xMm';
56

67
interface CCAEventProperties {
78
template?: string; // can be example, source, or JS or TS
8-
manager: 'npm' | 'yarn';
9+
manager: PackageManager;
910
interactive: boolean;
1011
}
1112

packages/contentful--create-contentful-app/src/getTemplateSource.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,10 @@ async function promptExampleSelection(): Promise<string> {
4545
// get available templates from examples
4646
const availableTemplates = await getGithubFolderNames();
4747
// filter out the ignored ones that are listed as templates instead of examples
48-
const filteredTemplates = availableTemplates
49-
.filter(
50-
(template) =>
51-
!IGNORED_EXAMPLE_FOLDERS.includes(template as (typeof IGNORED_EXAMPLE_FOLDERS)[number])
52-
)
48+
const filteredTemplates = availableTemplates.filter(
49+
(template) =>
50+
!IGNORED_EXAMPLE_FOLDERS.includes(template as (typeof IGNORED_EXAMPLE_FOLDERS)[number])
51+
);
5352
console.log(availableTemplates.length, filteredTemplates.length);
5453

5554
// ask user to select a template from the available examples

packages/contentful--create-contentful-app/src/index.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ import { program } from 'commander';
77
import inquirer from 'inquirer';
88
import tildify from 'tildify';
99
import { cloneTemplateIn } from './template';
10-
import { detectManager, exec, normalizeOptions, isContentfulTemplate } from './utils';
11-
import { CLIOptions } from './types';
10+
import {
11+
detectActivePackageManager,
12+
getNormalizedPackageManager,
13+
exec,
14+
normalizeOptions,
15+
isContentfulTemplate,
16+
} from './utils';
17+
import type { CLIOptions, PackageManager } from './types';
1218
import { code, error, highlight, success, warn, wrapInBlanks } from './logger';
1319
import chalk from 'chalk';
1420
import { CREATE_APP_DEFINITION_GUIDE_URL, EXAMPLES_REPO_URL } from './constants';
@@ -19,7 +25,15 @@ import fs from 'fs';
1925

2026
const DEFAULT_APP_NAME = 'contentful-app';
2127

22-
function successMessage(folder: string, useYarn: boolean) {
28+
function successMessage(folder: string, packageManager: PackageManager) {
29+
let command = '';
30+
if (packageManager === 'yarn') {
31+
command = 'yarn create-app-definition';
32+
} else if (packageManager === 'pnpm') {
33+
command = 'pnpm create-app-definition';
34+
} else {
35+
command = 'npm run create-app-definition';
36+
}
2337
console.log(`
2438
${success('Success!')} Created a new Contentful app in ${highlight(tildify(folder))}.`);
2539

@@ -28,7 +42,7 @@ ${success('Success!')} Created a new Contentful app in ${highlight(tildify(folde
2842
console.log(`Now create an app definition for your app by running
2943
3044
${code(`cd ${tildify(folder)}`)}
31-
${code(useYarn ? 'yarn create-app-definition' : 'npm run create-app-definition')}
45+
${code(command)}
3246
3347
or you can create it manually in web app:
3448
${highlight(CREATE_APP_DEFINITION_GUIDE_URL)}
@@ -37,7 +51,7 @@ ${success('Success!')} Created a new Contentful app in ${highlight(tildify(folde
3751
console.log(`Then kick it off by running
3852
3953
${code(`cd ${tildify(folder)}`)}
40-
${code(`${useYarn ? 'yarn' : 'npm'} start`)}
54+
${code(`${packageManager} start`)}
4155
`);
4256
}
4357

@@ -100,6 +114,9 @@ async function validateAppName(appName: string): Promise<string> {
100114

101115
async function initProject(appName: string, options: CLIOptions) {
102116
const normalizedOptions = normalizeOptions(options);
117+
const activePackageManager = detectActivePackageManager();
118+
const packageManager = getNormalizedPackageManager(normalizedOptions, activePackageManager);
119+
103120
try {
104121
appName = await validateAppName(appName);
105122

@@ -115,28 +132,28 @@ async function initProject(appName: string, options: CLIOptions) {
115132

116133
updatePackageName(fullAppFolder);
117134

118-
const useYarn = normalizedOptions.yarn || detectManager() === 'yarn';
119-
120135
wrapInBlanks(
121136
highlight(
122-
`---- Installing the dependencies for your app (using ${chalk.cyan(
123-
useYarn ? 'yarn' : 'npm'
124-
)})...`
137+
`---- Installing the dependencies for your app (using ${chalk.cyan(packageManager)})...`
125138
)
126139
);
127-
if (useYarn) {
140+
141+
if (packageManager === 'yarn') {
128142
await exec('yarn', [], { cwd: fullAppFolder });
143+
} else if (packageManager === 'pnpm') {
144+
await exec('pnpm', ['install'], { cwd: fullAppFolder });
129145
} else {
130146
await exec('npm', ['install', '--no-audit', '--no-fund'], { cwd: fullAppFolder });
131147
}
132-
successMessage(fullAppFolder, useYarn);
148+
successMessage(fullAppFolder, packageManager);
133149
} catch (err) {
134150
error(`Failed to create ${highlight(chalk.cyan(appName))}`, err);
135151
process.exit(1);
136152
}
137153

138154
async function addAppExample(fullAppFolder: string) {
139-
const isInteractive = !normalizedOptions.example &&
155+
const isInteractive =
156+
!normalizedOptions.example &&
140157
!normalizedOptions.source &&
141158
!normalizedOptions.javascript &&
142159
!normalizedOptions.typescript &&
@@ -146,7 +163,7 @@ async function initProject(appName: string, options: CLIOptions) {
146163

147164
track({
148165
template: templateSource,
149-
manager: normalizedOptions.npm ? 'npm' : 'yarn',
166+
manager: packageManager,
150167
interactive: isInteractive,
151168
});
152169

@@ -163,7 +180,7 @@ async function initProject(appName: string, options: CLIOptions) {
163180
}
164181

165182
async function addFunctionTemplate(fullAppFolder: string) {
166-
if (!fs.existsSync(fullAppFolder)) {
183+
if (!fs.existsSync(fullAppFolder)) {
167184
fs.mkdirSync(fullAppFolder, { recursive: true });
168185
}
169186

@@ -188,7 +205,7 @@ async function initProject(appName: string, options: CLIOptions) {
188205
example: normalizedOptions.function,
189206
language: normalizedOptions.javascript ? 'javascript' : 'typescript',
190207
name: functionName,
191-
keepPackageJson: normalizedOptions.skipUi === true
208+
keepPackageJson: normalizedOptions.skipUi === true,
192209
} as any);
193210
}
194211
}
@@ -212,6 +229,7 @@ async function initProject(appName: string, options: CLIOptions) {
212229
)
213230
.argument('[app-name]', 'app name')
214231
.option('--npm', 'use npm')
232+
.option('--pnpm', 'use pnpm')
215233
.option('--yarn', 'use Yarn')
216234
.option('-ts, --typescript', 'use TypeScript template (default)')
217235
.option('-js, --javascript', 'use JavaScript template')

packages/contentful--create-contentful-app/src/template.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function validate(destination: string): void {
3636
}
3737

3838
function cleanUp(destination: string) {
39+
rmIfExists(resolve(destination, 'pnpm-lock.json'));
3940
rmIfExists(resolve(destination, 'package-lock.json'));
4041
rmIfExists(resolve(destination, 'yarn.lock'));
4142
}

packages/contentful--create-contentful-app/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type CLIOptions = Partial<{
22
npm: boolean;
33
yarn: boolean;
4+
pnpm: boolean;
45
javascript: boolean;
56
typescript: boolean;
67
source: string;
@@ -9,6 +10,8 @@ export type CLIOptions = Partial<{
910
skipUi: boolean;
1011
}>;
1112

13+
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
14+
1215
export const ContentfulExample = {
1316
Javascript: 'javascript',
1417
Typescript: 'typescript',

packages/contentful--create-contentful-app/src/utils.ts

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
2-
import { existsSync, rmSync } from 'fs';
2+
import { existsSync, readFileSync, rmSync } from 'fs';
33
import { basename } from 'path';
44
import { choice, highlight, warn } from './logger';
5-
import { CLIOptions, ContentfulExample } from './types';
5+
import { CLIOptions, ContentfulExample, PackageManager } from './types';
66
import { EXAMPLES_PATH } from './constants';
77

88
const MUTUALLY_EXCLUSIVE_OPTIONS = ['source', 'example', 'typescript', 'javascript'] as const;
@@ -26,31 +26,82 @@ export function rmIfExists(path: string) {
2626
}
2727
}
2828

29-
export function detectManager() {
29+
export function detectActivePackageManager(): PackageManager {
30+
if (existsSync('pnpm-lock.yaml')) return 'pnpm';
31+
if (existsSync('yarn.lock')) return 'yarn';
32+
if (existsSync('package-lock.json')) return 'npm';
33+
warn('No lock files found, we will try to detect the active package manager from package.json.');
34+
try {
35+
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
36+
if (pkg.packageManager?.startsWith('pnpm')) return 'pnpm';
37+
if (pkg.packageManager?.startsWith('yarn')) return 'yarn';
38+
if (pkg.packageManager?.startsWith('npm')) return 'npm';
39+
} catch {
40+
warn(
41+
`Unable to determine active package manager from package.json. We will try to detect it from npm_execpath.`
42+
);
43+
}
44+
3045
switch (basename(process.env.npm_execpath || '')) {
3146
case 'yarn.js':
3247
return 'yarn';
48+
case 'pnpm.cjs':
49+
return 'pnpm';
3350
case 'npx-cli.js':
3451
case 'npm-cli.js':
3552
default:
3653
return 'npm';
3754
}
3855
}
3956

57+
// By the time this function is called, the options have already been normalized
58+
// so we would not need to consider multiple package manager flags at once
59+
export function getNormalizedPackageManager(
60+
options: CLIOptions,
61+
activePackageManager: PackageManager
62+
): PackageManager {
63+
// Prefer to get the package manager from options
64+
if (options.pnpm) {
65+
return 'pnpm';
66+
} else if (options.yarn) {
67+
return 'yarn';
68+
} else if (options.npm) {
69+
return 'npm';
70+
}
71+
72+
// Fallback to active package manager
73+
return activePackageManager;
74+
}
75+
4076
export function normalizeOptions(options: CLIOptions): CLIOptions {
4177
const normalizedOptions: CLIOptions = { ...options };
4278

43-
if (normalizedOptions.npm && normalizedOptions.yarn) {
79+
const selectedPackageManagers = [
80+
['npm', normalizedOptions.npm],
81+
['pnpm', normalizedOptions.pnpm],
82+
['yarn', normalizedOptions.yarn],
83+
].filter(([, n]) => n);
84+
const activePackageManager = detectActivePackageManager();
85+
86+
if (selectedPackageManagers.length > 1) {
4487
warn(
45-
`Provided both ${highlight('--yarn')} and ${highlight('--npm')} flags, using ${choice(
46-
'--npm'
88+
`Too many package manager flags were provided, we will use ${choice(
89+
`--${activePackageManager}`
4790
)}.`
4891
);
49-
delete normalizedOptions.yarn;
92+
93+
// Delete all package manager options
94+
selectedPackageManagers.forEach(([packageManager]) => {
95+
delete normalizedOptions[packageManager as keyof CLIOptions];
96+
});
97+
98+
// Select active package manager
99+
(normalizedOptions as CLIOptions)[activePackageManager] = true;
50100
}
51101

52-
if (!normalizedOptions.yarn) {
53-
normalizedOptions.npm = true;
102+
// No package manager flags were provided, use active package manager
103+
if (selectedPackageManagers.length === 0) {
104+
(normalizedOptions as CLIOptions)[activePackageManager] = true;
54105
}
55106

56107
let fallbackOption = '--typescript';

0 commit comments

Comments
 (0)