Skip to content

Commit a28a319

Browse files
committed
feat: Add Bun as supported package manager
1 parent 4cf3b18 commit a28a319

File tree

10 files changed

+185
-0
lines changed

10 files changed

+185
-0
lines changed

actions/new.action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ const askForPackageManager = async (): Promise<Answers> => {
168168
PackageManager.NPM,
169169
PackageManager.YARN,
170170
PackageManager.PNPM,
171+
PackageManager.BUN,
171172
]),
172173
];
173174
const prompt = inquirer.createPromptModule();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Runner, RunnerFactory } from '../runners';
2+
import { BunRunner } from '../runners/bun.runner';
3+
import { AbstractPackageManager } from './abstract.package-manager';
4+
import { PackageManager } from './package-manager';
5+
import { PackageManagerCommands } from './package-manager-commands';
6+
7+
export class BunPackageManager extends AbstractPackageManager {
8+
constructor() {
9+
super(RunnerFactory.create(Runner.BUN) as BunRunner);
10+
}
11+
12+
public get name() {
13+
return PackageManager.BUN.toUpperCase();
14+
}
15+
16+
get cli(): PackageManagerCommands {
17+
return {
18+
install: 'install',
19+
add: 'add',
20+
update: 'install --force',
21+
remove: 'remove',
22+
saveFlag: '',
23+
saveDevFlag: '--development',
24+
silentFlag: '--silent',
25+
};
26+
}
27+
}

lib/package-managers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from './abstract.package-manager';
44
export * from './npm.package-manager';
55
export * from './yarn.package-manager';
66
export * from './pnpm.package-manager';
7+
export * from './bun.package-manager';
78
export * from './project.dependency';
89
export * from './package-manager-commands';

lib/package-managers/package-manager.factory.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NpmPackageManager } from './npm.package-manager';
44
import { PackageManager } from './package-manager';
55
import { YarnPackageManager } from './yarn.package-manager';
66
import { PnpmPackageManager } from './pnpm.package-manager';
7+
import { BunPackageManager } from './bun.package-manager';
78

89
export class PackageManagerFactory {
910
public static create(name: PackageManager | string): AbstractPackageManager {
@@ -14,6 +15,8 @@ export class PackageManagerFactory {
1415
return new YarnPackageManager();
1516
case PackageManager.PNPM:
1617
return new PnpmPackageManager();
18+
case PackageManager.BUN:
19+
return new BunPackageManager();
1720
default:
1821
throw new Error(`Package manager ${name} is not managed.`);
1922
}
@@ -35,6 +38,11 @@ export class PackageManagerFactory {
3538
return this.create(PackageManager.PNPM);
3639
}
3740

41+
const hasBunLockFile = files.includes('bun.lockb');
42+
if (hasBunLockFile) {
43+
return this.create(PackageManager.BUN);
44+
}
45+
3846
return this.create(DEFAULT_PACKAGE_MANAGER);
3947
} catch (error) {
4048
return this.create(DEFAULT_PACKAGE_MANAGER);

lib/package-managers/package-manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export enum PackageManager {
22
NPM = 'npm',
33
YARN = 'yarn',
44
PNPM = 'pnpm',
5+
BUN = 'bun',
56
}

lib/runners/bun.runner.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { AbstractRunner } from './abstract.runner';
2+
3+
export class BunRunner extends AbstractRunner {
4+
constructor() {
5+
super('bun');
6+
}
7+
}

lib/runners/runner.factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Runner } from './runner';
44
import { SchematicRunner } from './schematic.runner';
55
import { YarnRunner } from './yarn.runner';
66
import { PnpmRunner } from './pnpm.runner';
7+
import { BunRunner } from './bun.runner';
78

89
export class RunnerFactory {
910
public static create(runner: Runner) {
@@ -20,6 +21,9 @@ export class RunnerFactory {
2021
case Runner.PNPM:
2122
return new PnpmRunner();
2223

24+
case Runner.BUN:
25+
return new BunRunner();
26+
2327
default:
2428
console.info(chalk.yellow(`[WARN] Unsupported runner: ${runner}`));
2529
}

lib/runners/runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export enum Runner {
33
NPM,
44
YARN,
55
PNPM,
6+
BUN,
67
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { join } from 'path';
2+
import {
3+
PackageManagerCommands,
4+
BunPackageManager,
5+
} from '../../../lib/package-managers';
6+
import { BunRunner } from '../../../lib/runners/bun.runner';
7+
8+
jest.mock('../../../lib/runners/bun.runner');
9+
10+
describe('BunPackageManager', () => {
11+
let packageManager: BunPackageManager;
12+
beforeEach(() => {
13+
(BunRunner as any).mockClear();
14+
(BunRunner as any).mockImplementation(() => {
15+
return {
16+
run: (): Promise<void> => Promise.resolve(),
17+
};
18+
});
19+
packageManager = new BunPackageManager();
20+
});
21+
it('should be created', () => {
22+
expect(packageManager).toBeInstanceOf(BunPackageManager);
23+
});
24+
it('should have the correct cli commands', () => {
25+
const expectedValues: PackageManagerCommands = {
26+
install: 'install',
27+
add: 'add',
28+
update: 'install --force',
29+
remove: 'remove',
30+
saveFlag: '',
31+
saveDevFlag: '--development',
32+
silentFlag: '--silent',
33+
};
34+
expect(packageManager.cli).toMatchObject(expectedValues);
35+
});
36+
describe('install', () => {
37+
it('should use the proper command for installing', () => {
38+
const spy = jest.spyOn((packageManager as any).runner, 'run');
39+
const dirName = '/tmp';
40+
const testDir = join(process.cwd(), dirName);
41+
packageManager.install(dirName, 'bun');
42+
expect(spy).toBeCalledWith('install --silent', true, testDir);
43+
});
44+
});
45+
describe('addProduction', () => {
46+
it('should use the proper command for adding production dependencies', () => {
47+
const spy = jest.spyOn((packageManager as any).runner, 'run');
48+
const dependencies = ['@nestjs/common', '@nestjs/core'];
49+
const tag = '5.0.0';
50+
const command = `add ${dependencies
51+
.map((dependency) => `${dependency}@${tag}`)
52+
.join(' ')}`;
53+
packageManager.addProduction(dependencies, tag);
54+
expect(spy).toBeCalledWith(command, true);
55+
});
56+
});
57+
describe('addDevelopment', () => {
58+
it('should use the proper command for adding development dependencies', () => {
59+
const spy = jest.spyOn((packageManager as any).runner, 'run');
60+
const dependencies = ['@nestjs/common', '@nestjs/core'];
61+
const tag = '5.0.0';
62+
const command = `add --development ${dependencies
63+
.map((dependency) => `${dependency}@${tag}`)
64+
.join(' ')}`;
65+
packageManager.addDevelopment(dependencies, tag);
66+
expect(spy).toBeCalledWith(command, true);
67+
});
68+
});
69+
describe('updateProduction', () => {
70+
it('should use the proper command for updating production dependencies', () => {
71+
const spy = jest.spyOn((packageManager as any).runner, 'run');
72+
const dependencies = ['@nestjs/common', '@nestjs/core'];
73+
const command = `install --force ${dependencies.join(' ')}`;
74+
packageManager.updateProduction(dependencies);
75+
expect(spy).toBeCalledWith(command, true);
76+
});
77+
});
78+
describe('updateDevelopment', () => {
79+
it('should use the proper command for updating development dependencies', () => {
80+
const spy = jest.spyOn((packageManager as any).runner, 'run');
81+
const dependencies = ['@nestjs/common', '@nestjs/core'];
82+
const command = `install --force ${dependencies.join(' ')}`;
83+
packageManager.updateDevelopment(dependencies);
84+
expect(spy).toBeCalledWith(command, true);
85+
});
86+
});
87+
describe('upgradeProduction', () => {
88+
it('should use the proper command for upgrading production dependencies', () => {
89+
const spy = jest.spyOn((packageManager as any).runner, 'run');
90+
const dependencies = ['@nestjs/common', '@nestjs/core'];
91+
const tag = '5.0.0';
92+
const uninstallCommand = `remove ${dependencies.join(' ')}`;
93+
94+
const installCommand = `add ${dependencies
95+
.map((dependency) => `${dependency}@${tag}`)
96+
.join(' ')}`;
97+
98+
return packageManager.upgradeProduction(dependencies, tag).then(() => {
99+
expect(spy.mock.calls).toEqual([
100+
[uninstallCommand, true],
101+
[installCommand, true],
102+
]);
103+
});
104+
});
105+
});
106+
describe('upgradeDevelopment', () => {
107+
it('should use the proper command for upgrading production dependencies', () => {
108+
const spy = jest.spyOn((packageManager as any).runner, 'run');
109+
const dependencies = ['@nestjs/common', '@nestjs/core'];
110+
const tag = '5.0.0';
111+
const uninstallCommand = `remove --development ${dependencies.join(' ')}`;
112+
113+
const installCommand = `add --development ${dependencies
114+
.map((dependency) => `${dependency}@${tag}`)
115+
.join(' ')}`;
116+
117+
return packageManager.upgradeDevelopment(dependencies, tag).then(() => {
118+
expect(spy.mock.calls).toEqual([
119+
[uninstallCommand, true],
120+
[installCommand, true],
121+
]);
122+
});
123+
});
124+
});
125+
});

test/lib/package-managers/package-manager.factory.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
PackageManagerFactory,
55
PnpmPackageManager,
66
YarnPackageManager,
7+
BunPackageManager,
78
} from '../../../lib/package-managers';
89

910
jest.mock('fs', () => ({
@@ -45,6 +46,15 @@ describe('PackageManagerFactory', () => {
4546
);
4647
});
4748

49+
it('should return BunPackageManager when "bun.lockb" file is found', async () => {
50+
(fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lockb']);
51+
52+
const whenPackageManager = PackageManagerFactory.find();
53+
await expect(whenPackageManager).resolves.toBeInstanceOf(
54+
BunPackageManager,
55+
);
56+
});
57+
4858
describe('when there are all supported lock files', () => {
4959
it('should prioritize "yarn.lock" file over all the others lock files', async () => {
5060
(fs.promises.readdir as jest.Mock).mockResolvedValue([

0 commit comments

Comments
 (0)