Skip to content

Commit bde44f7

Browse files
feat: support-pnpm-package-manager [ZEND-6808]
Based on [this PR](#2631) , with minor tweaks.
1 parent 4a3afc0 commit bde44f7

File tree

3 files changed

+213
-27
lines changed

3 files changed

+213
-27
lines changed

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/utils.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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';
55
import { CLIOptions, ContentfulExample, PackageManager } from './types';
@@ -27,6 +27,21 @@ export function rmIfExists(path: string) {
2727
}
2828

2929
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';
@@ -70,7 +85,9 @@ export function normalizeOptions(options: CLIOptions): CLIOptions {
7085

7186
if (selectedPackageManagers.length > 1) {
7287
warn(
73-
`Too many package manager flags were provided, we will use ${choice(`--${activePackageManager}`)}.`
88+
`Too many package manager flags were provided, we will use ${choice(
89+
`--${activePackageManager}`
90+
)}.`
7491
);
7592

7693
// Delete all package manager options
@@ -79,12 +96,12 @@ export function normalizeOptions(options: CLIOptions): CLIOptions {
7996
});
8097

8198
// Select active package manager
82-
(normalizedOptions as Record<string, boolean>)[activePackageManager] = true;
99+
(normalizedOptions as CLIOptions)[activePackageManager] = true;
83100
}
84101

85102
// No package manager flags were provided, use active package manager
86103
if (selectedPackageManagers.length === 0) {
87-
(normalizedOptions as Record<string, boolean>)[activePackageManager] = true;
104+
(normalizedOptions as CLIOptions)[activePackageManager] = true;
88105
}
89106

90107
let fallbackOption = '--typescript';

packages/contentful--create-contentful-app/test/utils.spec.ts

Lines changed: 188 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/// <reference types="node" />
2+
// The above directive tells TypeScript to include Node.js type definitions (i.e. process)
13
import { expect } from 'chai';
24
import {
35
detectActivePackageManager,
@@ -11,44 +13,166 @@ describe('utils', () => {
1113

1214
describe('detectActivePackageManager', () => {
1315
let originalNpmExecpath: string | undefined;
16+
let originalCwd: string;
17+
let tempDir: string;
1418

1519
beforeEach(() => {
1620
originalNpmExecpath = process.env.npm_execpath;
21+
originalCwd = process.cwd();
22+
23+
// Create temporary directory for each test
24+
const fs = require('fs');
25+
const os = require('os');
26+
const path = require('path');
27+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-test-'));
28+
process.chdir(tempDir);
1729
});
1830

1931
afterEach(() => {
32+
// Restore environment
2033
if (originalNpmExecpath) {
2134
process.env.npm_execpath = originalNpmExecpath;
2235
} else {
2336
delete process.env.npm_execpath;
2437
}
38+
39+
// Restore working directory and cleanup
40+
process.chdir(originalCwd);
41+
try {
42+
const fs = require('fs');
43+
fs.rmSync(tempDir, { recursive: true, force: true });
44+
} catch (error) {
45+
// Ignore cleanup errors
46+
}
2547
});
2648

27-
[
28-
['yarn', '/path/to/yarn.js'],
29-
['pnpm', '/path/to/pnpm.cjs'],
30-
['npm', '/path/to/npx-cli.js'],
31-
['npm', '/path/to/npm-cli.js'],
32-
].forEach(([packageManager, npmExecpath]) => {
33-
it(`should detect ${packageManager} when npm_execpath contains ${npmExecpath}`, () => {
34-
process.env.npm_execpath = npmExecpath;
35-
expect(detectActivePackageManager()).to.equal(packageManager);
49+
describe('lock file detection', () => {
50+
it('should detect pnpm from pnpm-lock.yaml', () => {
51+
const fs = require('fs');
52+
fs.writeFileSync('pnpm-lock.yaml', 'lockfileVersion: 5.4');
53+
expect(detectActivePackageManager()).to.equal('pnpm');
54+
});
55+
56+
it('should detect yarn from yarn.lock', () => {
57+
const fs = require('fs');
58+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
59+
expect(detectActivePackageManager()).to.equal('yarn');
60+
});
61+
62+
it('should detect npm from package-lock.json', () => {
63+
const fs = require('fs');
64+
fs.writeFileSync('package-lock.json', '{"lockfileVersion": 2}');
65+
expect(detectActivePackageManager()).to.equal('npm');
66+
});
67+
68+
it('should prioritize pnpm-lock.yaml over other lock files', () => {
69+
const fs = require('fs');
70+
fs.writeFileSync('pnpm-lock.yaml', 'lockfileVersion: 5.4');
71+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
72+
fs.writeFileSync('package-lock.json', '{"lockfileVersion": 2}');
73+
expect(detectActivePackageManager()).to.equal('pnpm');
74+
});
75+
76+
it('should prioritize yarn.lock over package-lock.json', () => {
77+
const fs = require('fs');
78+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
79+
fs.writeFileSync('package-lock.json', '{"lockfileVersion": 2}');
80+
expect(detectActivePackageManager()).to.equal('yarn');
3681
});
3782
});
3883

39-
it('should default to npm when npm_execpath is undefined', () => {
40-
delete process.env.npm_execpath;
41-
expect(detectActivePackageManager()).to.equal('npm');
84+
describe('package.json packageManager field detection', () => {
85+
it('should detect pnpm from package.json packageManager field', () => {
86+
const fs = require('fs');
87+
const packageJson = { packageManager: 'pnpm@8.6.0' };
88+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
89+
expect(detectActivePackageManager()).to.equal('pnpm');
90+
});
91+
92+
it('should detect yarn from package.json packageManager field', () => {
93+
const fs = require('fs');
94+
const packageJson = { packageManager: 'yarn@1.22.19' };
95+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
96+
expect(detectActivePackageManager()).to.equal('yarn');
97+
});
98+
99+
it('should detect npm from package.json packageManager field', () => {
100+
const fs = require('fs');
101+
const packageJson = { packageManager: 'npm@9.8.0' };
102+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
103+
expect(detectActivePackageManager()).to.equal('npm');
104+
});
105+
106+
it('should prioritize lock files over package.json packageManager field', () => {
107+
const fs = require('fs');
108+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
109+
const packageJson = { packageManager: 'pnpm@8.6.0' };
110+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
111+
expect(detectActivePackageManager()).to.equal('yarn');
112+
});
42113
});
43114

44-
it('should default to npm when npm_execpath is empty string', () => {
45-
process.env.npm_execpath = '';
46-
expect(detectActivePackageManager()).to.equal('npm');
115+
describe('npm_execpath fallback detection', () => {
116+
[
117+
['yarn', '/path/to/yarn.js'],
118+
['pnpm', '/path/to/pnpm.cjs'],
119+
['npm', '/path/to/npx-cli.js'],
120+
['npm', '/path/to/npm-cli.js'],
121+
].forEach(([packageManager, npmExecpath]) => {
122+
it(`should detect ${packageManager} when npm_execpath contains ${npmExecpath}`, () => {
123+
process.env.npm_execpath = npmExecpath;
124+
expect(detectActivePackageManager()).to.equal(packageManager);
125+
});
126+
});
127+
128+
it('should default to npm when npm_execpath is undefined', () => {
129+
delete process.env.npm_execpath;
130+
expect(detectActivePackageManager()).to.equal('npm');
131+
});
132+
133+
it('should default to npm when npm_execpath is empty string', () => {
134+
process.env.npm_execpath = '';
135+
expect(detectActivePackageManager()).to.equal('npm');
136+
});
137+
138+
it('should default to npm for unknown execpath', () => {
139+
process.env.npm_execpath = '/path/to/unknown-package-manager.js';
140+
expect(detectActivePackageManager()).to.equal('npm');
141+
});
142+
143+
it('should fall back to npm_execpath when package.json is invalid', () => {
144+
const fs = require('fs');
145+
fs.writeFileSync('package.json', 'invalid json');
146+
process.env.npm_execpath = '/path/to/yarn.js';
147+
expect(detectActivePackageManager()).to.equal('yarn');
148+
});
149+
150+
it('should fall back to npm_execpath when package.json has no packageManager field', () => {
151+
const fs = require('fs');
152+
const packageJson = { name: 'test-app', version: '1.0.0' };
153+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
154+
process.env.npm_execpath = '/path/to/pnpm.cjs';
155+
expect(detectActivePackageManager()).to.equal('pnpm');
156+
});
47157
});
48158

49-
it('should default to npm for unknown execpath', () => {
50-
process.env.npm_execpath = '/path/to/unknown-package-manager.js';
51-
expect(detectActivePackageManager()).to.equal('npm');
159+
describe('priority and edge cases', () => {
160+
it('should prioritize lock files over package.json over npm_execpath', () => {
161+
const fs = require('fs');
162+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
163+
const packageJson = { packageManager: 'pnpm@8.6.0' };
164+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
165+
process.env.npm_execpath = '/path/to/npm-cli.js';
166+
expect(detectActivePackageManager()).to.equal('yarn');
167+
});
168+
169+
it('should prioritize package.json over npm_execpath when no lock files exist', () => {
170+
const fs = require('fs');
171+
const packageJson = { packageManager: 'pnpm@8.6.0' };
172+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
173+
process.env.npm_execpath = '/path/to/yarn.js';
174+
expect(detectActivePackageManager()).to.equal('pnpm');
175+
});
52176
});
53177
});
54178

@@ -96,17 +220,37 @@ describe('utils', () => {
96220

97221
describe('normalizeOptions', () => {
98222
let originalNpmExecpath: string | undefined;
223+
let originalCwd: string;
224+
let tempDir: string;
99225

100226
beforeEach(() => {
101227
originalNpmExecpath = process.env.npm_execpath;
228+
originalCwd = process.cwd();
229+
230+
// Create temporary directory for each test
231+
const fs = require('fs');
232+
const os = require('os');
233+
const path = require('path');
234+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'normalize-test-'));
235+
process.chdir(tempDir);
102236
});
103237

104238
afterEach(() => {
239+
// Restore environment
105240
if (originalNpmExecpath) {
106241
process.env.npm_execpath = originalNpmExecpath;
107242
} else {
108243
delete process.env.npm_execpath;
109244
}
245+
246+
// Restore working directory and cleanup
247+
process.chdir(originalCwd);
248+
try {
249+
const fs = require('fs');
250+
fs.rmSync(tempDir, { recursive: true, force: true });
251+
} catch (error) {
252+
// Ignore cleanup errors
253+
}
110254
});
111255

112256
describe('no package manager options are provided', () => {
@@ -116,6 +260,7 @@ describe('utils', () => {
116260
['npm', '/path/to/npx-cli.js'],
117261
].forEach(([activePackageManager, npmExecpath]) => {
118262
it(`falls back to ${activePackageManager} when that is the active package manager`, () => {
263+
// Set npm_execpath to force detection to use fallback
119264
process.env.npm_execpath = npmExecpath;
120265
const options: CLIOptions = {};
121266
const result = normalizeOptions(options);
@@ -128,6 +273,31 @@ describe('utils', () => {
128273
});
129274
});
130275
});
276+
277+
it('falls back to detected package manager from lock files', () => {
278+
const fs = require('fs');
279+
fs.writeFileSync('yarn.lock', '# yarn lockfile v1');
280+
281+
const options: CLIOptions = {};
282+
const result = normalizeOptions(options);
283+
284+
expect(result.yarn).to.be.true;
285+
expect(result.npm).to.be.undefined;
286+
expect(result.pnpm).to.be.undefined;
287+
});
288+
289+
it('falls back to detected package manager from package.json', () => {
290+
const fs = require('fs');
291+
const packageJson = { packageManager: 'pnpm@8.6.0' };
292+
fs.writeFileSync('package.json', JSON.stringify(packageJson));
293+
294+
const options: CLIOptions = {};
295+
const result = normalizeOptions(options);
296+
297+
expect(result.pnpm).to.be.true;
298+
expect(result.npm).to.be.undefined;
299+
expect(result.yarn).to.be.undefined;
300+
});
131301
});
132302

133303
describe('one package manager option is provided', () => {

0 commit comments

Comments
 (0)