diff --git a/actions/new.action.ts b/actions/new.action.ts index 99c3fef50..7b167bb75 100644 --- a/actions/new.action.ts +++ b/actions/new.action.ts @@ -168,6 +168,7 @@ const askForPackageManager = async (): Promise => { PackageManager.NPM, PackageManager.YARN, PackageManager.PNPM, + PackageManager.BUN, ]), ]; const prompt = inquirer.createPromptModule(); diff --git a/lib/package-managers/bun.package-manager.ts b/lib/package-managers/bun.package-manager.ts new file mode 100644 index 000000000..1fe26a8a0 --- /dev/null +++ b/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', + }; + } +} diff --git a/lib/package-managers/index.ts b/lib/package-managers/index.ts index 9ada765f3..79601a4ec 100644 --- a/lib/package-managers/index.ts +++ b/lib/package-managers/index.ts @@ -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'; diff --git a/lib/package-managers/package-manager.factory.ts b/lib/package-managers/package-manager.factory.ts index ea3dd7561..2e46b0097 100644 --- a/lib/package-managers/package-manager.factory.ts +++ b/lib/package-managers/package-manager.factory.ts @@ -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 { @@ -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.`); } @@ -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); diff --git a/lib/package-managers/package-manager.ts b/lib/package-managers/package-manager.ts index 7d2b857d2..3a692c2f1 100644 --- a/lib/package-managers/package-manager.ts +++ b/lib/package-managers/package-manager.ts @@ -2,4 +2,5 @@ export enum PackageManager { NPM = 'npm', YARN = 'yarn', PNPM = 'pnpm', + BUN = 'bun', } diff --git a/lib/runners/bun.runner.ts b/lib/runners/bun.runner.ts new file mode 100644 index 000000000..767378cd9 --- /dev/null +++ b/lib/runners/bun.runner.ts @@ -0,0 +1,7 @@ +import { AbstractRunner } from './abstract.runner'; + +export class BunRunner extends AbstractRunner { + constructor() { + super('bun'); + } +} diff --git a/lib/runners/runner.factory.ts b/lib/runners/runner.factory.ts index 6b8ce3fa0..9b96430ab 100644 --- a/lib/runners/runner.factory.ts +++ b/lib/runners/runner.factory.ts @@ -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) { @@ -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}`)); } diff --git a/lib/runners/runner.ts b/lib/runners/runner.ts index 74b5c2eae..fffaae2ca 100644 --- a/lib/runners/runner.ts +++ b/lib/runners/runner.ts @@ -3,4 +3,5 @@ export enum Runner { NPM, YARN, PNPM, + BUN, } diff --git a/test/lib/package-managers/bun.package-manager.spec.ts b/test/lib/package-managers/bun.package-manager.spec.ts new file mode 100644 index 000000000..4c5e831f2 --- /dev/null +++ b/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 => 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], + ]); + }); + }); + }); +}); diff --git a/test/lib/package-managers/package-manager.factory.spec.ts b/test/lib/package-managers/package-manager.factory.spec.ts index 01748ddb3..a0fb0422f 100644 --- a/test/lib/package-managers/package-manager.factory.spec.ts +++ b/test/lib/package-managers/package-manager.factory.spec.ts @@ -4,6 +4,7 @@ import { PackageManagerFactory, PnpmPackageManager, YarnPackageManager, + BunPackageManager, } from '../../../lib/package-managers'; jest.mock('fs', () => ({ @@ -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([