Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 63 additions & 73 deletions packages/core/src/lib/auto-installer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
});

Expand All @@ -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',
});
});

Expand Down Expand Up @@ -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',
});
});

Expand Down Expand Up @@ -164,37 +163,34 @@ 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',
});
});

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

expect(execSync).toHaveBeenCalledWith(
'npm install --save-dev package1 package2',
'npm install -D package1 package2',
{ stdio: 'inherit' }
);
});

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',
});
});
Expand All @@ -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',
});
});

Expand All @@ -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();
});
});
});
Expand Down
38 changes: 15 additions & 23 deletions packages/core/src/lib/auto-installer.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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' });
Expand Down