Skip to content

Commit e631837

Browse files
michaelbe812claude
andcommitted
fix(core): detect package manager properly using @nx/devkit
Replace custom package manager detection heuristic with @nx/devkit's detectPackageManager and getPackageManagerCommand functions. The previous implementation checked for globally available binaries (pnpm -v, yarn -v) which didn't respect the project's actual package manager configuration based on lock files or packageManager field in package.json. Now correctly detects: - npm when package-lock.json exists - yarn when yarn.lock exists - pnpm when pnpm-lock.yaml exists - Also respects packageManager field in package.json Closes #74 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 06111d3 commit e631837

2 files changed

Lines changed: 78 additions & 96 deletions

File tree

packages/core/src/lib/auto-installer.spec.ts

Lines changed: 63 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ import {
44
detectPackageManager,
55
installPackages,
66
} from './auto-installer';
7+
import {
8+
detectPackageManager as nxDetectPackageManager,
9+
getPackageManagerCommand,
10+
} from '@nx/devkit';
711

812
// Mock node:child_process
913
jest.mock('node:child_process', () => ({
1014
execSync: jest.fn(),
1115
}));
1216

17+
// Mock @nx/devkit
18+
jest.mock('@nx/devkit', () => ({
19+
detectPackageManager: jest.fn(),
20+
getPackageManagerCommand: jest.fn(),
21+
}));
22+
1323
describe('auto-installer', () => {
1424
const originalEnv = process.env;
1525

@@ -48,43 +58,33 @@ describe('auto-installer', () => {
4858
});
4959

5060
describe('detectPackageManager', () => {
51-
it('should detect pnpm when available', () => {
52-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
53-
if (cmd === 'pnpm -v') return;
54-
throw new Error('Command not found');
55-
});
61+
it('should detect pnpm when project uses pnpm-lock.yaml', () => {
62+
(nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm');
5663

5764
expect(detectPackageManager()).toBe('pnpm');
58-
expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' });
65+
expect(nxDetectPackageManager).toHaveBeenCalled();
5966
});
6067

61-
it('should detect yarn when pnpm is not available', () => {
62-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
63-
if (cmd === 'yarn -v') return;
64-
throw new Error('Command not found');
65-
});
68+
it('should detect yarn when project uses yarn.lock', () => {
69+
(nxDetectPackageManager as jest.Mock).mockReturnValue('yarn');
6670

6771
expect(detectPackageManager()).toBe('yarn');
68-
expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' });
69-
expect(execSync).toHaveBeenCalledWith('yarn -v', { stdio: 'ignore' });
72+
expect(nxDetectPackageManager).toHaveBeenCalled();
7073
});
7174

72-
it('should default to npm when neither pnpm nor yarn is available', () => {
73-
(execSync as jest.Mock).mockImplementation(() => {
74-
throw new Error('Command not found');
75-
});
75+
it('should detect npm when project uses package-lock.json', () => {
76+
(nxDetectPackageManager as jest.Mock).mockReturnValue('npm');
7677

7778
expect(detectPackageManager()).toBe('npm');
78-
expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' });
79-
expect(execSync).toHaveBeenCalledWith('yarn -v', { stdio: 'ignore' });
79+
expect(nxDetectPackageManager).toHaveBeenCalled();
8080
});
8181

82-
it('should handle execution errors gracefully', () => {
83-
(execSync as jest.Mock).mockImplementation(() => {
84-
throw new Error('Some error');
85-
});
82+
it('should delegate to @nx/devkit detectPackageManager', () => {
83+
(nxDetectPackageManager as jest.Mock).mockReturnValue('npm');
8684

87-
expect(detectPackageManager()).toBe('npm');
85+
detectPackageManager();
86+
87+
expect(nxDetectPackageManager).toHaveBeenCalledTimes(1);
8888
});
8989
});
9090

@@ -95,10 +95,10 @@ describe('auto-installer', () => {
9595

9696
describe('with pnpm', () => {
9797
beforeEach(() => {
98-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
99-
if (cmd === 'pnpm -v') return;
100-
if (cmd.startsWith('pnpm add')) return;
101-
throw new Error('Command not found');
98+
(nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm');
99+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
100+
add: 'pnpm add',
101+
addDev: 'pnpm add -D',
102102
});
103103
});
104104

@@ -129,11 +129,10 @@ describe('auto-installer', () => {
129129

130130
describe('with yarn', () => {
131131
beforeEach(() => {
132-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
133-
if (cmd === 'pnpm -v') throw new Error('Not found');
134-
if (cmd === 'yarn -v') return;
135-
if (cmd.startsWith('yarn add')) return;
136-
throw new Error('Command not found');
132+
(nxDetectPackageManager as jest.Mock).mockReturnValue('yarn');
133+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
134+
add: 'yarn add',
135+
addDev: 'yarn add -D',
137136
});
138137
});
139138

@@ -164,37 +163,34 @@ describe('auto-installer', () => {
164163

165164
describe('with npm', () => {
166165
beforeEach(() => {
167-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
168-
if (cmd === 'pnpm -v' || cmd === 'yarn -v') {
169-
throw new Error('Not found');
170-
}
171-
if (cmd.startsWith('npm install')) return;
172-
throw new Error('Command not found');
166+
(nxDetectPackageManager as jest.Mock).mockReturnValue('npm');
167+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
168+
add: 'npm install',
169+
addDev: 'npm install -D',
173170
});
174171
});
175172

176173
it('should install dev dependencies by default', () => {
177174
installPackages(['package1', 'package2']);
178175

179176
expect(execSync).toHaveBeenCalledWith(
180-
'npm install --save-dev package1 package2',
177+
'npm install -D package1 package2',
181178
{ stdio: 'inherit' }
182179
);
183180
});
184181

185182
it('should install dev dependencies when dev is true', () => {
186183
installPackages(['package1'], { dev: true });
187184

188-
expect(execSync).toHaveBeenCalledWith(
189-
'npm install --save-dev package1',
190-
{ stdio: 'inherit' }
191-
);
185+
expect(execSync).toHaveBeenCalledWith('npm install -D package1', {
186+
stdio: 'inherit',
187+
});
192188
});
193189

194190
it('should install regular dependencies when dev is false', () => {
195191
installPackages(['package1'], { dev: false });
196192

197-
expect(execSync).toHaveBeenCalledWith('npm install package1', {
193+
expect(execSync).toHaveBeenCalledWith('npm install package1', {
198194
stdio: 'inherit',
199195
});
200196
});
@@ -203,52 +199,48 @@ describe('auto-installer', () => {
203199
describe('CI environment', () => {
204200
beforeEach(() => {
205201
process.env.CI = 'true';
202+
(nxDetectPackageManager as jest.Mock).mockReturnValue('npm');
203+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
204+
add: 'npm install',
205+
addDev: 'npm install -D',
206+
});
206207
});
207208

208209
it('should not install packages in CI environment', () => {
209210
installPackages(['package1']);
210211

211-
expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' });
212-
expect(execSync).not.toHaveBeenCalledWith(
213-
expect.stringContaining('add'),
214-
expect.any(Object)
215-
);
216-
expect(execSync).not.toHaveBeenCalledWith(
217-
expect.stringContaining('install'),
218-
expect.any(Object)
219-
);
212+
expect(execSync).not.toHaveBeenCalled();
220213
});
221214
});
222215

223216
describe('error handling', () => {
224217
beforeEach(() => {
225218
delete process.env.CI;
226-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
227-
if (cmd === 'pnpm -v' || cmd === 'yarn -v') {
228-
throw new Error('Not found');
229-
}
230-
if (cmd.startsWith('npm install')) {
231-
throw new Error('Installation failed');
232-
}
219+
(nxDetectPackageManager as jest.Mock).mockReturnValue('npm');
220+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
221+
add: 'npm install',
222+
addDev: 'npm install -D',
223+
});
224+
(execSync as jest.Mock).mockImplementation(() => {
225+
throw new Error('Installation failed');
233226
});
234227
});
235228

236229
it('should handle installation errors gracefully', () => {
237230
expect(() => installPackages(['package1'])).not.toThrow();
238231

239-
expect(execSync).toHaveBeenCalledWith(
240-
'npm install --save-dev package1',
241-
{ stdio: 'inherit' }
242-
);
232+
expect(execSync).toHaveBeenCalledWith('npm install -D package1', {
233+
stdio: 'inherit',
234+
});
243235
});
244236
});
245237

246238
describe('multiple packages', () => {
247239
beforeEach(() => {
248-
(execSync as jest.Mock).mockImplementation((cmd: string) => {
249-
if (cmd === 'pnpm -v') return;
250-
if (cmd.startsWith('pnpm add')) return;
251-
throw new Error('Command not found');
240+
(nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm');
241+
(getPackageManagerCommand as jest.Mock).mockReturnValue({
242+
add: 'pnpm add',
243+
addDev: 'pnpm add -D',
252244
});
253245
});
254246

@@ -264,10 +256,8 @@ describe('auto-installer', () => {
264256
it('should handle empty package list', () => {
265257
installPackages([]);
266258

267-
expect(execSync).not.toHaveBeenCalledWith(
268-
expect.stringContaining('add'),
269-
expect.any(Object)
270-
);
259+
expect(execSync).not.toHaveBeenCalled();
260+
expect(getPackageManagerCommand).not.toHaveBeenCalled();
271261
});
272262
});
273263
});

packages/core/src/lib/auto-installer.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import { execSync } from 'node:child_process';
2+
import {
3+
detectPackageManager as nxDetectPackageManager,
4+
getPackageManagerCommand,
5+
type PackageManager,
6+
} from '@nx/devkit';
27

3-
export type PackageManager = 'npm' | 'pnpm' | 'yarn';
8+
export type { PackageManager };
49

510
export function detectCi(): boolean {
611
return Boolean(process.env['CI']);
712
}
813

14+
/**
15+
* Detects the package manager used by the project.
16+
* Uses @nx/devkit which properly checks lock files (pnpm-lock.yaml, yarn.lock, package-lock.json)
17+
* and the packageManager field in package.json.
18+
*/
919
export function detectPackageManager(): PackageManager {
10-
// Minimal heuristic; can be expanded later
11-
try {
12-
execSync('pnpm -v', { stdio: 'ignore' });
13-
return 'pnpm';
14-
} catch {
15-
/* noop */
16-
}
17-
try {
18-
execSync('yarn -v', { stdio: 'ignore' });
19-
return 'yarn';
20-
} catch {
21-
/* noop */
22-
}
23-
return 'npm';
20+
return nxDetectPackageManager();
2421
}
2522

2623
export function installPackages(
@@ -29,15 +26,10 @@ export function installPackages(
2926
): void {
3027
if (pkgs.length === 0) return;
3128

32-
const pm = detectPackageManager();
29+
const pmc = getPackageManagerCommand();
3330
const dev = opts?.dev ?? true;
34-
const devFlag = dev ? (pm === 'yarn' ? '-D' : '--save-dev') : '';
35-
const cmd =
36-
pm === 'pnpm'
37-
? `pnpm add ${dev ? '-D ' : ''}${pkgs.join(' ')}`
38-
: pm === 'yarn'
39-
? `yarn add ${dev ? '-D ' : ''}${pkgs.join(' ')}`
40-
: `npm install ${devFlag} ${pkgs.join(' ')}`;
31+
const cmd = `${dev ? pmc.addDev : pmc.add} ${pkgs.join(' ')}`;
32+
4133
if (!detectCi()) {
4234
try {
4335
execSync(cmd, { stdio: 'inherit' });

0 commit comments

Comments
 (0)