Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Bun as supported package manager #2223

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions actions/new.action.ts
Expand Up @@ -168,6 +168,7 @@ const askForPackageManager = async (): Promise<Answers> => {
PackageManager.NPM,
PackageManager.YARN,
PackageManager.PNPM,
PackageManager.BUN,
]),
];
const prompt = inquirer.createPromptModule();
Expand Down
27 changes: 27 additions & 0 deletions lib/package-managers/bun.package-manager.ts
@@ -0,0 +1,27 @@
import { Runner, RunnerFactory } from '../runners';
import { BunRunner } from '../runners/bun.runner';
import { AbstractPackageManager } from './abstract.package-manager';
import { PackageManager } from './package-manager';
import { PackageManagerCommands } from './package-manager-commands';

export class BunPackageManager extends AbstractPackageManager {
constructor() {
super(RunnerFactory.create(Runner.BUN) as BunRunner);
}

public get name() {
return PackageManager.BUN.toUpperCase();
}

get cli(): PackageManagerCommands {
return {
install: 'install',
add: 'add',
update: 'install --force',
remove: 'remove',
saveFlag: '',
saveDevFlag: '--development',
silentFlag: '--silent',
};
}
}
1 change: 1 addition & 0 deletions lib/package-managers/index.ts
Expand Up @@ -4,5 +4,6 @@ export * from './abstract.package-manager';
export * from './npm.package-manager';
export * from './yarn.package-manager';
export * from './pnpm.package-manager';
export * from './bun.package-manager';
export * from './project.dependency';
export * from './package-manager-commands';
8 changes: 8 additions & 0 deletions lib/package-managers/package-manager.factory.ts
Expand Up @@ -4,6 +4,7 @@ import { NpmPackageManager } from './npm.package-manager';
import { PackageManager } from './package-manager';
import { YarnPackageManager } from './yarn.package-manager';
import { PnpmPackageManager } from './pnpm.package-manager';
import { BunPackageManager } from './bun.package-manager';

export class PackageManagerFactory {
public static create(name: PackageManager | string): AbstractPackageManager {
Expand All @@ -14,6 +15,8 @@ export class PackageManagerFactory {
return new YarnPackageManager();
case PackageManager.PNPM:
return new PnpmPackageManager();
case PackageManager.BUN:
return new BunPackageManager();
default:
throw new Error(`Package manager ${name} is not managed.`);
}
Expand All @@ -35,6 +38,11 @@ export class PackageManagerFactory {
return this.create(PackageManager.PNPM);
}

const hasBunLockFile = files.includes('bun.lockb');
if (hasBunLockFile) {
return this.create(PackageManager.BUN);
}

return this.create(DEFAULT_PACKAGE_MANAGER);
} catch (error) {
return this.create(DEFAULT_PACKAGE_MANAGER);
Expand Down
1 change: 1 addition & 0 deletions lib/package-managers/package-manager.ts
Expand Up @@ -2,4 +2,5 @@ export enum PackageManager {
NPM = 'npm',
YARN = 'yarn',
PNPM = 'pnpm',
BUN = 'bun',
}
7 changes: 7 additions & 0 deletions lib/runners/bun.runner.ts
@@ -0,0 +1,7 @@
import { AbstractRunner } from './abstract.runner';

export class BunRunner extends AbstractRunner {
constructor() {
super('bun');
}
}
4 changes: 4 additions & 0 deletions lib/runners/runner.factory.ts
Expand Up @@ -4,6 +4,7 @@ import { Runner } from './runner';
import { SchematicRunner } from './schematic.runner';
import { YarnRunner } from './yarn.runner';
import { PnpmRunner } from './pnpm.runner';
import { BunRunner } from './bun.runner';

export class RunnerFactory {
public static create(runner: Runner) {
Expand All @@ -20,6 +21,9 @@ export class RunnerFactory {
case Runner.PNPM:
return new PnpmRunner();

case Runner.BUN:
return new BunRunner();

default:
console.info(chalk.yellow(`[WARN] Unsupported runner: ${runner}`));
}
Expand Down
1 change: 1 addition & 0 deletions lib/runners/runner.ts
Expand Up @@ -3,4 +3,5 @@ export enum Runner {
NPM,
YARN,
PNPM,
BUN,
}
125 changes: 125 additions & 0 deletions test/lib/package-managers/bun.package-manager.spec.ts
@@ -0,0 +1,125 @@
import { join } from 'path';
import {
PackageManagerCommands,
BunPackageManager,
} from '../../../lib/package-managers';
import { BunRunner } from '../../../lib/runners/bun.runner';

jest.mock('../../../lib/runners/bun.runner');

describe('BunPackageManager', () => {
let packageManager: BunPackageManager;
beforeEach(() => {
(BunRunner as any).mockClear();
(BunRunner as any).mockImplementation(() => {
return {
run: (): Promise<void> => Promise.resolve(),
};
});
packageManager = new BunPackageManager();
});
it('should be created', () => {
expect(packageManager).toBeInstanceOf(BunPackageManager);
});
it('should have the correct cli commands', () => {
const expectedValues: PackageManagerCommands = {
install: 'install',
add: 'add',
update: 'install --force',
remove: 'remove',
saveFlag: '',
saveDevFlag: '--development',
silentFlag: '--silent',
};
expect(packageManager.cli).toMatchObject(expectedValues);
});
describe('install', () => {
it('should use the proper command for installing', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dirName = '/tmp';
const testDir = join(process.cwd(), dirName);
packageManager.install(dirName, 'bun');
expect(spy).toBeCalledWith('install --silent', true, testDir);
});
});
describe('addProduction', () => {
it('should use the proper command for adding production dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const tag = '5.0.0';
const command = `add ${dependencies
.map((dependency) => `${dependency}@${tag}`)
.join(' ')}`;
packageManager.addProduction(dependencies, tag);
expect(spy).toBeCalledWith(command, true);
});
});
describe('addDevelopment', () => {
it('should use the proper command for adding development dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const tag = '5.0.0';
const command = `add --development ${dependencies
.map((dependency) => `${dependency}@${tag}`)
.join(' ')}`;
packageManager.addDevelopment(dependencies, tag);
expect(spy).toBeCalledWith(command, true);
});
});
describe('updateProduction', () => {
it('should use the proper command for updating production dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const command = `install --force ${dependencies.join(' ')}`;
packageManager.updateProduction(dependencies);
expect(spy).toBeCalledWith(command, true);
});
});
describe('updateDevelopment', () => {
it('should use the proper command for updating development dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const command = `install --force ${dependencies.join(' ')}`;
packageManager.updateDevelopment(dependencies);
expect(spy).toBeCalledWith(command, true);
});
});
describe('upgradeProduction', () => {
it('should use the proper command for upgrading production dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const tag = '5.0.0';
const uninstallCommand = `remove ${dependencies.join(' ')}`;

const installCommand = `add ${dependencies
.map((dependency) => `${dependency}@${tag}`)
.join(' ')}`;

return packageManager.upgradeProduction(dependencies, tag).then(() => {
expect(spy.mock.calls).toEqual([
[uninstallCommand, true],
[installCommand, true],
]);
});
});
});
describe('upgradeDevelopment', () => {
it('should use the proper command for upgrading production dependencies', () => {
const spy = jest.spyOn((packageManager as any).runner, 'run');
const dependencies = ['@nestjs/common', '@nestjs/core'];
const tag = '5.0.0';
const uninstallCommand = `remove --development ${dependencies.join(' ')}`;

const installCommand = `add --development ${dependencies
.map((dependency) => `${dependency}@${tag}`)
.join(' ')}`;

return packageManager.upgradeDevelopment(dependencies, tag).then(() => {
expect(spy.mock.calls).toEqual([
[uninstallCommand, true],
[installCommand, true],
]);
});
});
});
});
10 changes: 10 additions & 0 deletions test/lib/package-managers/package-manager.factory.spec.ts
Expand Up @@ -4,6 +4,7 @@ import {
PackageManagerFactory,
PnpmPackageManager,
YarnPackageManager,
BunPackageManager,
} from '../../../lib/package-managers';

jest.mock('fs', () => ({
Expand Down Expand Up @@ -45,6 +46,15 @@ describe('PackageManagerFactory', () => {
);
});

it('should return BunPackageManager when "bun.lockb" file is found', async () => {
(fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lockb']);

const whenPackageManager = PackageManagerFactory.find();
await expect(whenPackageManager).resolves.toBeInstanceOf(
BunPackageManager,
);
});

describe('when there are all supported lock files', () => {
it('should prioritize "yarn.lock" file over all the others lock files', async () => {
(fs.promises.readdir as jest.Mock).mockResolvedValue([
Expand Down