Skip to content

Commit 58d61e0

Browse files
committed
feat(package-managers): add bun support
1 parent 2ce8f9b commit 58d61e0

10 files changed

Lines changed: 198 additions & 1 deletion

File tree

actions/new.action.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ const installPackages = async (
170170
const askForPackageManager = async () => {
171171
const question = generateSelect('packageManager')(
172172
MESSAGES.PACKAGE_MANAGER_QUESTION,
173-
)([PackageManager.NPM, PackageManager.YARN, PackageManager.PNPM]);
173+
)([
174+
PackageManager.NPM,
175+
PackageManager.YARN,
176+
PackageManager.PNPM,
177+
PackageManager.BUN,
178+
]);
174179

175180
return select(question).catch(gracefullyExitOnPromptError);
176181
};
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: 'update',
21+
remove: 'remove',
22+
saveFlag: '--save',
23+
saveDevFlag: '--dev',
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: 9 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,12 @@ export class PackageManagerFactory {
3538
return this.create(PackageManager.PNPM);
3639
}
3740

41+
const hasBunLockFile =
42+
files.includes('bun.lock') || files.includes('bun.lockb');
43+
if (hasBunLockFile) {
44+
return this.create(PackageManager.BUN);
45+
}
46+
3847
return this.create(DEFAULT_PACKAGE_MANAGER);
3948
} catch (error) {
4049
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(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+
BunPackageManager,
4+
PackageManagerCommands,
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: 'update',
29+
remove: 'remove',
30+
saveFlag: '',
31+
saveDevFlag: '-d',
32+
silentFlag: '--quiet',
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, 'npm');
42+
expect(spy).toBeCalledWith('install --quiet', 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 -d ${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 = `update ${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 = `update ${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 development 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 -d ${dependencies.join(' ')}`;
112+
113+
const installCommand = `add -d ${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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from 'fs';
22
import {
3+
BunPackageManager,
34
NpmPackageManager,
45
PackageManagerFactory,
56
PnpmPackageManager,
@@ -45,11 +46,27 @@ describe('PackageManagerFactory', () => {
4546
);
4647
});
4748

49+
it('should return BunPackageManager when "bun.lock" file is found', async () => {
50+
(fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lock']);
51+
52+
const manager = await PackageManagerFactory.find();
53+
expect(manager).toBeInstanceOf(BunPackageManager);
54+
});
55+
56+
it('should return BunPackageManager when "bun.lockb" file is found', async () => {
57+
(fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lockb']);
58+
59+
const manager = await PackageManagerFactory.find();
60+
expect(manager).toBeInstanceOf(BunPackageManager);
61+
});
62+
4863
describe('when there are all supported lock files', () => {
4964
it('should prioritize "yarn.lock" file over all the others lock files', async () => {
5065
(fs.promises.readdir as jest.Mock).mockResolvedValue([
5166
'pnpm-lock.yaml',
5267
'package-lock.json',
68+
'bun.lock',
69+
'bun.lockb',
5370
// This is intentionally the last element in this array
5471
'yarn.lock',
5572
]);

0 commit comments

Comments
 (0)