From 99421c3c9d2885b0d40841f9bde957450ab4fe94 Mon Sep 17 00:00:00 2001 From: Jordan Hall Date: Mon, 11 Sep 2023 21:50:15 +0100 Subject: [PATCH] feat(core): add bun package manager Bun uses yarn lock for it's binary file. Running the binary will produce the content of a yarn lock file (v1) --- docs/generated/cli/create-nx-workspace.md | 2 +- docs/generated/devkit/PackageManager.md | 2 +- .../nx/documents/create-nx-workspace.md | 2 +- e2e/utils/create-project-utils.ts | 11 +++-- e2e/utils/get-env-info.ts | 8 ++-- .../src/create-nx-workspace.test.ts | 11 +++-- .../src/internal-utils/prompts.ts | 1 + .../src/utils/package-manager.ts | 12 +++++- .../lib/create-application-files.ts | 1 + .../update-16-1-4/update-eas-scripts.ts | 1 + packages/nx/schemas/nx-schema.json | 2 +- .../init/implementation/react/index.ts | 1 + packages/nx/src/plugins/js/index.ts | 11 ++++- .../nx/src/plugins/js/lock-file/lock-file.ts | 33 ++++++++++++++- packages/nx/src/utils/package-manager.spec.ts | 29 ++++++++++++- packages/nx/src/utils/package-manager.ts | 41 ++++++++++++++++--- .../ci-workflow/ci-workflow.spec.ts | 4 +- 17 files changed, 146 insertions(+), 26 deletions(-) diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index 4292def25a1e33..c013a280b22da2 100644 --- a/docs/generated/cli/create-nx-workspace.md +++ b/docs/generated/cli/create-nx-workspace.md @@ -129,7 +129,7 @@ Do you want Nx Cloud to make your CI fast? Type: `string` -Choices: [npm, pnpm, yarn] +Choices: [bun, npm, pnpm, yarn] Default: `npm` diff --git a/docs/generated/devkit/PackageManager.md b/docs/generated/devkit/PackageManager.md index e633d8c96104f1..0bcc0a12921588 100644 --- a/docs/generated/devkit/PackageManager.md +++ b/docs/generated/devkit/PackageManager.md @@ -1,3 +1,3 @@ # Type alias: PackageManager -Ƭ **PackageManager**: `"yarn"` \| `"pnpm"` \| `"npm"` +Ƭ **PackageManager**: `"yarn"` \| `"pnpm"` \| `"npm"` \| `"bun"` diff --git a/docs/generated/packages/nx/documents/create-nx-workspace.md b/docs/generated/packages/nx/documents/create-nx-workspace.md index 4292def25a1e33..c013a280b22da2 100644 --- a/docs/generated/packages/nx/documents/create-nx-workspace.md +++ b/docs/generated/packages/nx/documents/create-nx-workspace.md @@ -129,7 +129,7 @@ Do you want Nx Cloud to make your CI fast? Type: `string` -Choices: [npm, pnpm, yarn] +Choices: [bun, npm, pnpm, yarn] Default: `npm` diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 6c4b6b24355e90..967d1250dcdebd 100644 --- a/e2e/utils/create-project-utils.ts +++ b/e2e/utils/create-project-utils.ts @@ -78,7 +78,7 @@ export function newProject({ packages, }: { name?: string; - packageManager?: 'npm' | 'yarn' | 'pnpm'; + packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun'; unsetProjectNameAndRootFormat?: boolean; readonly packages?: Array; } = {}): string { @@ -240,7 +240,7 @@ export function runCreateWorkspace( appName?: string; style?: string; base?: string; - packageManager?: 'npm' | 'yarn' | 'pnpm'; + packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun'; extraArgs?: string; useDetectedPm?: boolean; cwd?: string; @@ -358,7 +358,7 @@ export function runCreatePlugin( extraArgs, useDetectedPm = false, }: { - packageManager?: 'npm' | 'yarn' | 'pnpm'; + packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun'; extraArgs?: string; useDetectedPm?: boolean; } @@ -543,6 +543,11 @@ export function newLernaWorkspace({ ...json.resolutions, ...overrides, }; + } else if (packageManager === 'bun') { + json.overrides = { + ...json.resolutions, + ...overrides, + }; } else { json.overrides = overrides; } diff --git a/e2e/utils/get-env-info.ts b/e2e/utils/get-env-info.ts index 4798c1f717fdcb..1aa25d144a6429 100644 --- a/e2e/utils/get-env-info.ts +++ b/e2e/utils/get-env-info.ts @@ -23,7 +23,9 @@ export function getPublishedVersion(): string { } export function detectPackageManager(dir: string = ''): PackageManager { - return existsSync(join(dir, 'yarn.lock')) + return existsSync(join(dir, 'bun.lockb')) + ? 'bun' + : existsSync(join(dir, 'yarn.lock')) ? 'yarn' : existsSync(join(dir, 'pnpm-lock.yaml')) || existsSync(join(dir, 'pnpm-workspace.yaml')) @@ -64,8 +66,8 @@ export function isVerboseE2ERun() { export const e2eCwd = `${e2eRoot}/nx`; -export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' { - return (process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm') || 'npm'; +export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' { + return (process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm' | 'bun') || 'npm'; } export function getNpmMajorVersion(): string | undefined { diff --git a/e2e/workspace-create/src/create-nx-workspace.test.ts b/e2e/workspace-create/src/create-nx-workspace.test.ts index cc1e75811b663f..d66f839793e8e5 100644 --- a/e2e/workspace-create/src/create-nx-workspace.test.ts +++ b/e2e/workspace-create/src/create-nx-workspace.test.ts @@ -374,7 +374,7 @@ describe('create-nx-workspace', () => { }); describe('Use detected package manager', () => { - function setupProject(envPm: 'npm' | 'yarn' | 'pnpm') { + function setupProject(envPm: 'npm' | 'yarn' | 'pnpm' | 'bun') { process.env.SELECTED_PM = envPm; runCreateWorkspace(uniq('pm'), { preset: 'apps', @@ -389,7 +389,8 @@ describe('create-nx-workspace', () => { checkFilesExist(packageManagerLockFile['npm']); checkFilesDoNotExist( packageManagerLockFile['yarn'], - packageManagerLockFile['pnpm'] + packageManagerLockFile['pnpm'], + packageManagerLockFile['bun'] ); process.env.SELECTED_PM = packageManager; }, 90000); @@ -401,7 +402,8 @@ describe('create-nx-workspace', () => { checkFilesExist(packageManagerLockFile['pnpm']); checkFilesDoNotExist( packageManagerLockFile['yarn'], - packageManagerLockFile['npm'] + packageManagerLockFile['npm'], + packageManagerLockFile['bun'] ); process.env.SELECTED_PM = packageManager; }, 90000); @@ -414,7 +416,8 @@ describe('create-nx-workspace', () => { checkFilesExist(packageManagerLockFile['yarn']); checkFilesDoNotExist( packageManagerLockFile['pnpm'], - packageManagerLockFile['npm'] + packageManagerLockFile['npm'], + packageManagerLockFile['bun'] ); process.env.SELECTED_PM = packageManager; }, 90000); diff --git a/packages/create-nx-workspace/src/internal-utils/prompts.ts b/packages/create-nx-workspace/src/internal-utils/prompts.ts index 465183b4fd81d4..abb6ef2f81190d 100644 --- a/packages/create-nx-workspace/src/internal-utils/prompts.ts +++ b/packages/create-nx-workspace/src/internal-utils/prompts.ts @@ -110,6 +110,7 @@ export async function determinePackageManager( { name: 'npm', message: 'NPM' }, { name: 'yarn', message: 'Yarn' }, { name: 'pnpm', message: 'PNPM' }, + { name: 'bun', message: 'Bun' }, ], }, ]) diff --git a/packages/create-nx-workspace/src/utils/package-manager.ts b/packages/create-nx-workspace/src/utils/package-manager.ts index 5966089e91b5aa..f822058f4d8db0 100644 --- a/packages/create-nx-workspace/src/utils/package-manager.ts +++ b/packages/create-nx-workspace/src/utils/package-manager.ts @@ -7,12 +7,14 @@ import { join } from 'path'; * we duplicate the helper functions from @nx/workspace in this file. */ -export const packageManagerList = ['pnpm', 'yarn', 'npm'] as const; +export const packageManagerList = ['pnpm', 'yarn', 'npm', 'bun'] as const; export type PackageManager = typeof packageManagerList[number]; export function detectPackageManager(dir: string = ''): PackageManager { - return existsSync(join(dir, 'yarn.lock')) + return existsSync(join(dir, 'bun.lockb')) + ? 'bun' + : existsSync(join(dir, 'yarn.lock')) ? 'yarn' : existsSync(join(dir, 'pnpm-lock.yaml')) ? 'pnpm' @@ -73,6 +75,12 @@ export function getPackageManagerCommand( exec: 'npx', globalAdd: 'npm i -g', }; + case 'bun': + return { + install: 'bun install --silent', + exec: 'bunx', + globalAdd: 'bun install -g', + }; } } diff --git a/packages/expo/src/generators/application/lib/create-application-files.ts b/packages/expo/src/generators/application/lib/create-application-files.ts index 4efeee32010532..a8fed6092bf9cc 100644 --- a/packages/expo/src/generators/application/lib/create-application-files.ts +++ b/packages/expo/src/generators/application/lib/create-application-files.ts @@ -14,6 +14,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) { npm: 'package-lock.json', yarn: 'yarn.lock', pnpm: 'pnpm-lock.yaml', + bun: 'bun.lockb', }; const packageManager = detectPackageManager(host.root); const packageLockFile = packageManagerLockFile[packageManager]; diff --git a/packages/expo/src/migrations/update-16-1-4/update-eas-scripts.ts b/packages/expo/src/migrations/update-16-1-4/update-eas-scripts.ts index 77db80bcee71fe..61f473936a0991 100644 --- a/packages/expo/src/migrations/update-16-1-4/update-eas-scripts.ts +++ b/packages/expo/src/migrations/update-16-1-4/update-eas-scripts.ts @@ -19,6 +19,7 @@ export default function update(tree: Tree) { npm: 'package-lock.json', yarn: 'yarn.lock', pnpm: 'pnpm-lock.yaml', + bun: 'bun.lockb', }; for (const [name, config] of projects.entries()) { diff --git a/packages/nx/schemas/nx-schema.json b/packages/nx/schemas/nx-schema.json index 1eb695b29894a4..62afadea594597 100644 --- a/packages/nx/schemas/nx-schema.json +++ b/packages/nx/schemas/nx-schema.json @@ -366,7 +366,7 @@ "packageManager": { "type": "string", "description": "The default package manager to use.", - "enum": ["yarn", "pnpm", "npm"] + "enum": ["yarn", "pnpm", "npm", "bun"] } } }, diff --git a/packages/nx/src/command-line/init/implementation/react/index.ts b/packages/nx/src/command-line/init/implementation/react/index.ts index 3e7717209ac07d..33cbebfc2ff572 100644 --- a/packages/nx/src/command-line/init/implementation/react/index.ts +++ b/packages/nx/src/command-line/init/implementation/react/index.ts @@ -262,6 +262,7 @@ function moveFilesToTempWorkspace(options: NormalizedOptions) { options.packageManager === 'yarn' ? 'yarn.lock' : null, options.packageManager === 'pnpm' ? 'pnpm-lock.yaml' : null, options.packageManager === 'npm' ? 'package-lock.json' : null, + options.packageManager === 'bun' ? 'bun.lockb' : null, ]; const optionalCraFiles = ['README.md']; diff --git a/packages/nx/src/plugins/js/index.ts b/packages/nx/src/plugins/js/index.ts index 738013b4d40a3f..f0d44f93752e45 100644 --- a/packages/nx/src/plugins/js/index.ts +++ b/packages/nx/src/plugins/js/index.ts @@ -24,6 +24,7 @@ import { hashArray } from '../../hasher/file-hasher'; import { detectPackageManager } from '../../utils/package-manager'; import { workspaceRoot } from '../../utils/workspace-root'; import { nxVersion } from '../../utils/versions'; +import { execSync } from 'child_process'; export const name = 'nx/js/dependencies-and-lockfile'; @@ -51,7 +52,10 @@ export const createNodes: CreateNodes = [ } const lockFilePath = join(workspaceRoot, lockFile); - const lockFileContents = readFileSync(lockFilePath).toString(); + const lockFileContents = + packageManager !== 'bun' + ? readFileSync(lockFilePath).toString() + : execSync(lockFilePath).toString(); const lockFileHash = getLockFileHash(lockFileContents); if (!lockFileNeedsReprocessing(lockFileHash)) { @@ -91,7 +95,10 @@ export const createDependencies: CreateDependencies = ( parsedLockFile.externalNodes ) { const lockFilePath = join(workspaceRoot, getLockFileName(packageManager)); - const lockFileContents = readFileSync(lockFilePath).toString(); + const lockFileContents = + packageManager !== 'bun' + ? readFileSync(lockFilePath).toString() + : execSync(lockFilePath).toString(); const lockFileHash = getLockFileHash(lockFileContents); if (!lockFileNeedsReprocessing(lockFileHash)) { diff --git a/packages/nx/src/plugins/js/lock-file/lock-file.ts b/packages/nx/src/plugins/js/lock-file/lock-file.ts index 21687585aff122..248f55525b2672 100644 --- a/packages/nx/src/plugins/js/lock-file/lock-file.ts +++ b/packages/nx/src/plugins/js/lock-file/lock-file.ts @@ -45,11 +45,18 @@ import { const YARN_LOCK_FILE = 'yarn.lock'; const NPM_LOCK_FILE = 'package-lock.json'; const PNPM_LOCK_FILE = 'pnpm-lock.yaml'; -export const LOCKFILES = [YARN_LOCK_FILE, NPM_LOCK_FILE, PNPM_LOCK_FILE]; +const BUN_LOCK_FILE = 'bun.lockb'; +export const LOCKFILES = [ + YARN_LOCK_FILE, + NPM_LOCK_FILE, + PNPM_LOCK_FILE, + BUN_LOCK_FILE, +]; const YARN_LOCK_PATH = join(workspaceRoot, YARN_LOCK_FILE); const NPM_LOCK_PATH = join(workspaceRoot, NPM_LOCK_FILE); const PNPM_LOCK_PATH = join(workspaceRoot, PNPM_LOCK_FILE); +const BUN_LOCK_PATH = join(workspaceRoot, BUN_LOCK_FILE); /** * Parses lock file and maps dependencies and metadata to {@link LockFileGraph} @@ -73,6 +80,11 @@ export function getLockFileNodes( if (packageManager === 'npm') { return getNpmLockfileNodes(contents, lockFileHash); } + if (packageManager === 'bun') { + // bun uses yarn v1 for the file format + const packageJson = readJsonFile('package.json'); + return getYarnLockfileNodes(contents, lockFileHash, packageJson); + } } catch (e) { if (!isPostInstallProcess()) { output.error({ @@ -104,6 +116,10 @@ export function getLockFileDependencies( if (packageManager === 'npm') { return getNpmLockfileDependencies(contents, lockFileHash, context); } + if (packageManager === 'bun') { + // bun uses yarn v1 for the file format + return getYarnLockfileDependencies(contents, lockFileHash, context); + } } catch (e) { if (!isPostInstallProcess()) { output.error({ @@ -126,6 +142,9 @@ export function lockFileExists(packageManager: PackageManager): boolean { if (packageManager === 'npm') { return existsSync(NPM_LOCK_PATH); } + if (packageManager === 'bun') { + return existsSync(BUN_LOCK_PATH); + } throw new Error( `Unknown package manager ${packageManager} or lock file missing` ); @@ -146,6 +165,9 @@ export function getLockFileName(packageManager: PackageManager): string { if (packageManager === 'npm') { return NPM_LOCK_FILE; } + if (packageManager === 'bun') { + return BUN_LOCK_FILE; + } throw new Error(`Unknown package manager: ${packageManager}`); } @@ -159,6 +181,9 @@ function getLockFilePath(packageManager: PackageManager): string { if (packageManager === 'npm') { return NPM_LOCK_PATH; } + if (packageManager === 'bun') { + return BUN_LOCK_PATH; + } throw new Error(`Unknown package manager: ${packageManager}`); } @@ -191,6 +216,12 @@ export function createLockFile( const prunedGraph = pruneProjectGraph(graph, packageJson); return stringifyNpmLockfile(prunedGraph, content, normalizedPackageJson); } + if (packageManager === 'bun') { + output.log({ + title: + "Unable to create bun lock files. Run bun install it's just as quick", + }); + } } catch (e) { if (!isPostInstallProcess()) { const additionalInfo = [ diff --git a/packages/nx/src/utils/package-manager.spec.ts b/packages/nx/src/utils/package-manager.spec.ts index 3ba3c7d8acfe1b..cd4e7d83aa79df 100644 --- a/packages/nx/src/utils/package-manager.spec.ts +++ b/packages/nx/src/utils/package-manager.spec.ts @@ -30,6 +30,8 @@ describe('package-manager', () => { return false; case 'package-lock.json': return false; + case 'bun.lockb': + return false; default: return jest.requireActual('fs').existsSync(p); } @@ -49,6 +51,8 @@ describe('package-manager', () => { return true; case 'package-lock.json': return false; + case 'bun.lockb': + return false; default: return jest.requireActual('fs').existsSync(p); } @@ -58,6 +62,27 @@ describe('package-manager', () => { expect(fs.existsSync).toHaveBeenCalledTimes(3); }); + it('should detect bun package manager from bun.lockb', () => { + jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({}); + jest.spyOn(fs, 'existsSync').mockImplementation((p) => { + switch (p) { + case 'yarn.lock': + return false; + case 'pnpm-lock.yaml': + return false; + case 'package-lock.json': + return false; + case 'bun.lockb': + return true; + default: + return jest.requireActual('fs').existsSync(p); + } + }); + const packageManager = detectPackageManager(); + expect(packageManager).toEqual('bun'); + expect(fs.existsSync).toHaveBeenCalledTimes(3); + }); + it('should use npm package manager as default', () => { jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({}); jest.spyOn(fs, 'existsSync').mockImplementation((p) => { @@ -68,13 +93,15 @@ describe('package-manager', () => { return false; case 'package-lock.json': return false; + case 'bun.lockb': + return false; default: return jest.requireActual('fs').existsSync(p); } }); const packageManager = detectPackageManager(); expect(packageManager).toEqual('npm'); - expect(fs.existsSync).toHaveBeenCalledTimes(5); + expect(fs.existsSync).toHaveBeenCalledTimes(6); }); }); diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index eebfc8d33fedfb..bdc44b5ae7f8db 100644 --- a/packages/nx/src/utils/package-manager.ts +++ b/packages/nx/src/utils/package-manager.ts @@ -13,7 +13,7 @@ import { workspaceRoot } from './workspace-root'; const execAsync = promisify(exec); -export type PackageManager = 'yarn' | 'pnpm' | 'npm'; +export type PackageManager = 'yarn' | 'pnpm' | 'npm' | 'bun'; export interface PackageManagerCommands { preInstall?: string; @@ -36,7 +36,9 @@ export function detectPackageManager(dir: string = ''): PackageManager { const nxJson = readNxJson(); return ( nxJson.cli?.packageManager ?? - (existsSync(join(dir, 'yarn.lock')) + (existsSync(join(dir, 'bun.lockb')) + ? 'bun' + : existsSync(join(dir, 'yarn.lock')) ? 'yarn' : existsSync(join(dir, 'pnpm-lock.yaml')) ? 'pnpm' @@ -142,6 +144,20 @@ export function getPackageManagerCommand( list: 'npm ls', }; }, + bun: () => { + return { + install: 'bun install', + ciInstall: 'bun install --no-cache', + updateLockFile: 'bun install --frozen-lockfile', + add: 'bun install', + addDev: 'bun install -D', + rm: 'bun rm', + exec: 'bun', + dlx: 'bunx', + run: (script: string, args: string) => `bun run ${script} -- ${args}`, + list: 'bun pm ls', + }; + }, }; return commands[packageManager](); @@ -230,7 +246,12 @@ export function copyPackageManagerConfigurationFiles( root: string, destination: string ) { - for (const packageManagerConfigFile of ['.npmrc', '.yarnrc', '.yarnrc.yml']) { + for (const packageManagerConfigFile of [ + '.npmrc', + '.yarnrc', + '.yarnrc.yml', + 'bunfig.toml', + ]) { // f is an absolute path, including the {workspaceRoot}. const f = findFileInPackageJsonDirectory(packageManagerConfigFile, root); if (f) { @@ -255,6 +276,10 @@ export function copyPackageManagerConfigurationFiles( writeFileSync(destinationPath, updated); break; } + case 'bunfig.toml': { + copyFileSync(f, destinationPath); + break; + } } } } @@ -343,12 +368,16 @@ export async function packageRegistryView( args: string ): Promise { let pm = detectPackageManager(); - if (pm === 'yarn') { + if (pm === 'yarn' || pm === 'bun') { /** * yarn has `yarn info` but it behaves differently than (p)npm, * which makes it's usage unreliable * * @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 + * + * Bun has a pm ls function but it only relates to its lockfile + * and acts differently from all other package managers + * from Jarred: "it probably would be bun pm view " */ pm = 'npm'; } @@ -363,13 +392,15 @@ export async function packageRegistryPack( version: string ): Promise<{ tarballPath: string }> { let pm = detectPackageManager(); - if (pm === 'yarn') { + if (pm === 'yarn' || pm === 'bun') { /** * `(p)npm pack` will download a tarball of the specified version, * whereas `yarn` pack creates a tarball of the active workspace, so it * does not work for getting the content of a library. * * @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 + * + * bun doesn't currently support pack */ pm = 'npm'; } diff --git a/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts b/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts index 4a3f3ac74d0e19..61773e506cde6f 100644 --- a/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts +++ b/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts @@ -39,7 +39,7 @@ describe('CI Workflow generator', () => { vol.reset(); }); - ['npm', 'yarn', 'pnpm'].forEach((packageManager: PackageManager) => { + ['npm', 'yarn', 'pnpm', 'bun'].forEach((packageManager: PackageManager) => { describe(`with ${packageManager}`, () => { beforeEach(() => { let fileSys; @@ -47,6 +47,8 @@ describe('CI Workflow generator', () => { fileSys = { 'yarn.lock': '' }; } else if (packageManager === 'pnpm') { fileSys = { 'pnpm-lock.yaml': '' }; + } else if (packageManager === 'bun') { + fileSys = { 'bun.lockb': '' }; } else { fileSys = { 'package-lock.json': '' }; }