diff --git a/actions/new.action.ts b/actions/new.action.ts index 240b94209..d3a0c9c81 100644 --- a/actions/new.action.ts +++ b/actions/new.action.ts @@ -170,7 +170,12 @@ const installPackages = async ( const askForPackageManager = async () => { const question = generateSelect('packageManager')( MESSAGES.PACKAGE_MANAGER_QUESTION, - )([PackageManager.NPM, PackageManager.YARN, PackageManager.PNPM]); + )([ + PackageManager.NPM, + PackageManager.YARN, + PackageManager.PNPM, + PackageManager.BUN, + ]); return select(question).catch(gracefullyExitOnPromptError); }; diff --git a/lib/package-managers/bun.package-manager.ts b/lib/package-managers/bun.package-manager.ts new file mode 100644 index 000000000..78e032e54 --- /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: 'update', + remove: 'remove', + saveFlag: '--save', + saveDevFlag: '--dev', + 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..edc62b113 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,13 @@ export class PackageManagerFactory { return this.create(PackageManager.PNPM); } + const hasBunLockFile = ['bun.lock', 'bun.lockb'].some((lockFile) => + files.includes(lockFile), + ); + 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 9b2045f02..ee0bc32fb 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(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..6b0cefce4 --- /dev/null +++ b/test/lib/package-managers/bun.package-manager.spec.ts @@ -0,0 +1,125 @@ +import { join } from 'path'; +import { + BunPackageManager, + PackageManagerCommands, +} 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: 'update', + remove: 'remove', + saveFlag: '--save', + saveDevFlag: '--dev', + 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, 'npm'); + 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 --save ${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 --dev ${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 = `update ${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 = `update ${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 --save ${dependencies.join(' ')}`; + + const installCommand = `add --save ${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 development dependencies', () => { + const spy = jest.spyOn((packageManager as any).runner, 'run'); + const dependencies = ['@nestjs/common', '@nestjs/core']; + const tag = '5.0.0'; + const uninstallCommand = `remove --dev ${dependencies.join(' ')}`; + + const installCommand = `add --dev ${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..70a5d9271 100644 --- a/test/lib/package-managers/package-manager.factory.spec.ts +++ b/test/lib/package-managers/package-manager.factory.spec.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import { + BunPackageManager, NpmPackageManager, PackageManagerFactory, PnpmPackageManager, @@ -45,11 +46,27 @@ describe('PackageManagerFactory', () => { ); }); + it('should return BunPackageManager when "bun.lock" file is found', async () => { + (fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lock']); + + const manager = await PackageManagerFactory.find(); + expect(manager).toBeInstanceOf(BunPackageManager); + }); + + it('should return BunPackageManager when "bun.lockb" file is found', async () => { + (fs.promises.readdir as jest.Mock).mockResolvedValue(['bun.lockb']); + + const manager = await PackageManagerFactory.find(); + expect(manager).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([ 'pnpm-lock.yaml', 'package-lock.json', + 'bun.lock', + 'bun.lockb', // This is intentionally the last element in this array 'yarn.lock', ]);