diff --git a/packages/core/src/lib/auto-installer.spec.ts b/packages/core/src/lib/auto-installer.spec.ts index a162826..19fc814 100644 --- a/packages/core/src/lib/auto-installer.spec.ts +++ b/packages/core/src/lib/auto-installer.spec.ts @@ -4,12 +4,22 @@ import { detectPackageManager, installPackages, } from './auto-installer'; +import { + detectPackageManager as nxDetectPackageManager, + getPackageManagerCommand, +} from '@nx/devkit'; // Mock node:child_process jest.mock('node:child_process', () => ({ execSync: jest.fn(), })); +// Mock @nx/devkit +jest.mock('@nx/devkit', () => ({ + detectPackageManager: jest.fn(), + getPackageManagerCommand: jest.fn(), +})); + describe('auto-installer', () => { const originalEnv = process.env; @@ -48,43 +58,33 @@ describe('auto-installer', () => { }); describe('detectPackageManager', () => { - it('should detect pnpm when available', () => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v') return; - throw new Error('Command not found'); - }); + it('should detect pnpm when project uses pnpm-lock.yaml', () => { + (nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm'); expect(detectPackageManager()).toBe('pnpm'); - expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' }); + expect(nxDetectPackageManager).toHaveBeenCalled(); }); - it('should detect yarn when pnpm is not available', () => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'yarn -v') return; - throw new Error('Command not found'); - }); + it('should detect yarn when project uses yarn.lock', () => { + (nxDetectPackageManager as jest.Mock).mockReturnValue('yarn'); expect(detectPackageManager()).toBe('yarn'); - expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' }); - expect(execSync).toHaveBeenCalledWith('yarn -v', { stdio: 'ignore' }); + expect(nxDetectPackageManager).toHaveBeenCalled(); }); - it('should default to npm when neither pnpm nor yarn is available', () => { - (execSync as jest.Mock).mockImplementation(() => { - throw new Error('Command not found'); - }); + it('should detect npm when project uses package-lock.json', () => { + (nxDetectPackageManager as jest.Mock).mockReturnValue('npm'); expect(detectPackageManager()).toBe('npm'); - expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' }); - expect(execSync).toHaveBeenCalledWith('yarn -v', { stdio: 'ignore' }); + expect(nxDetectPackageManager).toHaveBeenCalled(); }); - it('should handle execution errors gracefully', () => { - (execSync as jest.Mock).mockImplementation(() => { - throw new Error('Some error'); - }); + it('should delegate to @nx/devkit detectPackageManager', () => { + (nxDetectPackageManager as jest.Mock).mockReturnValue('npm'); - expect(detectPackageManager()).toBe('npm'); + detectPackageManager(); + + expect(nxDetectPackageManager).toHaveBeenCalledTimes(1); }); }); @@ -95,10 +95,10 @@ describe('auto-installer', () => { describe('with pnpm', () => { beforeEach(() => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v') return; - if (cmd.startsWith('pnpm add')) return; - throw new Error('Command not found'); + (nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'pnpm add', + addDev: 'pnpm add -D', }); }); @@ -129,11 +129,10 @@ describe('auto-installer', () => { describe('with yarn', () => { beforeEach(() => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v') throw new Error('Not found'); - if (cmd === 'yarn -v') return; - if (cmd.startsWith('yarn add')) return; - throw new Error('Command not found'); + (nxDetectPackageManager as jest.Mock).mockReturnValue('yarn'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'yarn add', + addDev: 'yarn add -D', }); }); @@ -164,12 +163,10 @@ describe('auto-installer', () => { describe('with npm', () => { beforeEach(() => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v' || cmd === 'yarn -v') { - throw new Error('Not found'); - } - if (cmd.startsWith('npm install')) return; - throw new Error('Command not found'); + (nxDetectPackageManager as jest.Mock).mockReturnValue('npm'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'npm install', + addDev: 'npm install -D', }); }); @@ -177,7 +174,7 @@ describe('auto-installer', () => { installPackages(['package1', 'package2']); expect(execSync).toHaveBeenCalledWith( - 'npm install --save-dev package1 package2', + 'npm install -D package1 package2', { stdio: 'inherit' } ); }); @@ -185,16 +182,15 @@ describe('auto-installer', () => { it('should install dev dependencies when dev is true', () => { installPackages(['package1'], { dev: true }); - expect(execSync).toHaveBeenCalledWith( - 'npm install --save-dev package1', - { stdio: 'inherit' } - ); + expect(execSync).toHaveBeenCalledWith('npm install -D package1', { + stdio: 'inherit', + }); }); it('should install regular dependencies when dev is false', () => { installPackages(['package1'], { dev: false }); - expect(execSync).toHaveBeenCalledWith('npm install package1', { + expect(execSync).toHaveBeenCalledWith('npm install package1', { stdio: 'inherit', }); }); @@ -203,52 +199,48 @@ describe('auto-installer', () => { describe('CI environment', () => { beforeEach(() => { process.env.CI = 'true'; + (nxDetectPackageManager as jest.Mock).mockReturnValue('npm'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'npm install', + addDev: 'npm install -D', + }); }); it('should not install packages in CI environment', () => { installPackages(['package1']); - expect(execSync).toHaveBeenCalledWith('pnpm -v', { stdio: 'ignore' }); - expect(execSync).not.toHaveBeenCalledWith( - expect.stringContaining('add'), - expect.any(Object) - ); - expect(execSync).not.toHaveBeenCalledWith( - expect.stringContaining('install'), - expect.any(Object) - ); + expect(execSync).not.toHaveBeenCalled(); }); }); describe('error handling', () => { beforeEach(() => { delete process.env.CI; - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v' || cmd === 'yarn -v') { - throw new Error('Not found'); - } - if (cmd.startsWith('npm install')) { - throw new Error('Installation failed'); - } + (nxDetectPackageManager as jest.Mock).mockReturnValue('npm'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'npm install', + addDev: 'npm install -D', + }); + (execSync as jest.Mock).mockImplementation(() => { + throw new Error('Installation failed'); }); }); it('should handle installation errors gracefully', () => { expect(() => installPackages(['package1'])).not.toThrow(); - expect(execSync).toHaveBeenCalledWith( - 'npm install --save-dev package1', - { stdio: 'inherit' } - ); + expect(execSync).toHaveBeenCalledWith('npm install -D package1', { + stdio: 'inherit', + }); }); }); describe('multiple packages', () => { beforeEach(() => { - (execSync as jest.Mock).mockImplementation((cmd: string) => { - if (cmd === 'pnpm -v') return; - if (cmd.startsWith('pnpm add')) return; - throw new Error('Command not found'); + (nxDetectPackageManager as jest.Mock).mockReturnValue('pnpm'); + (getPackageManagerCommand as jest.Mock).mockReturnValue({ + add: 'pnpm add', + addDev: 'pnpm add -D', }); }); @@ -264,10 +256,8 @@ describe('auto-installer', () => { it('should handle empty package list', () => { installPackages([]); - expect(execSync).not.toHaveBeenCalledWith( - expect.stringContaining('add'), - expect.any(Object) - ); + expect(execSync).not.toHaveBeenCalled(); + expect(getPackageManagerCommand).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/core/src/lib/auto-installer.ts b/packages/core/src/lib/auto-installer.ts index 8ffe76b..c503a6b 100644 --- a/packages/core/src/lib/auto-installer.ts +++ b/packages/core/src/lib/auto-installer.ts @@ -1,26 +1,23 @@ import { execSync } from 'node:child_process'; +import { + detectPackageManager as nxDetectPackageManager, + getPackageManagerCommand, + type PackageManager, +} from '@nx/devkit'; -export type PackageManager = 'npm' | 'pnpm' | 'yarn'; +export type { PackageManager }; export function detectCi(): boolean { return Boolean(process.env['CI']); } +/** + * Detects the package manager used by the project. + * Uses @nx/devkit which properly checks lock files (pnpm-lock.yaml, yarn.lock, package-lock.json) + * and the packageManager field in package.json. + */ export function detectPackageManager(): PackageManager { - // Minimal heuristic; can be expanded later - try { - execSync('pnpm -v', { stdio: 'ignore' }); - return 'pnpm'; - } catch { - /* noop */ - } - try { - execSync('yarn -v', { stdio: 'ignore' }); - return 'yarn'; - } catch { - /* noop */ - } - return 'npm'; + return nxDetectPackageManager(); } export function installPackages( @@ -29,15 +26,10 @@ export function installPackages( ): void { if (pkgs.length === 0) return; - const pm = detectPackageManager(); + const pmc = getPackageManagerCommand(); const dev = opts?.dev ?? true; - const devFlag = dev ? (pm === 'yarn' ? '-D' : '--save-dev') : ''; - const cmd = - pm === 'pnpm' - ? `pnpm add ${dev ? '-D ' : ''}${pkgs.join(' ')}` - : pm === 'yarn' - ? `yarn add ${dev ? '-D ' : ''}${pkgs.join(' ')}` - : `npm install ${devFlag} ${pkgs.join(' ')}`; + const cmd = `${dev ? pmc.addDev : pmc.add} ${pkgs.join(' ')}`; + if (!detectCi()) { try { execSync(cmd, { stdio: 'inherit' });