diff --git a/docs/generated/cli/create-nx-workspace.md b/docs/generated/cli/create-nx-workspace.md index 007a4416334eac..e5de260ffc0725 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 007a4416334eac..e5de260ffc0725 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/docs/generated/packages/workspace/generators/new.json b/docs/generated/packages/workspace/generators/new.json index a1f3a1ea3dd382..4e69e3a9d9d0db 100644 --- a/docs/generated/packages/workspace/generators/new.json +++ b/docs/generated/packages/workspace/generators/new.json @@ -53,7 +53,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "framework": { "description": "The framework which the application is using", diff --git a/docs/generated/packages/workspace/generators/preset.json b/docs/generated/packages/workspace/generators/preset.json index ebcd47cd087447..b6dafd5b72c1f9 100644 --- a/docs/generated/packages/workspace/generators/preset.json +++ b/docs/generated/packages/workspace/generators/preset.json @@ -59,7 +59,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "framework": { "description": "The framework which the application is using", diff --git a/e2e/utils/command-utils.ts b/e2e/utils/command-utils.ts index bd3a087f10db46..0aeb47d0e7beee 100644 --- a/e2e/utils/command-utils.ts +++ b/e2e/utils/command-utils.ts @@ -171,6 +171,19 @@ export function getPackageManagerCommand({ list: 'pnpm ls --depth 10', runLerna: `pnpm exec lerna`, }, + bun: { + createWorkspace: `bunx create-nx-workspace@${publishedVersion}`, + run: (script: string, args: string) => `bun run ${script} -- ${args}`, + runNx: `bunx nx`, + runNxSilent: `bunx nx`, + runUninstalledPackage: `bunx --yes`, + install: 'bun install', + ciInstall: 'bun install --no-cache', + addProd: 'bun install', + addDev: 'bun install -D', + list: 'bun pm ls', + runLerna: `bunx lerna`, + }, }[packageManager.trim() as PackageManager]; } diff --git a/e2e/utils/create-project-utils.ts b/e2e/utils/create-project-utils.ts index 3f4ae3b429755c..11ef32228acb55 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..17e8f98a9a5d7e 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 { @@ -108,6 +110,7 @@ export const packageManagerLockFile = { npm: 'package-lock.json', yarn: 'yarn.lock', pnpm: 'pnpm-lock.yaml', + bun: 'bun.lockb', }; export function ensureCypressInstallation() { diff --git a/e2e/workspace-create/src/create-nx-workspace.test.ts b/e2e/workspace-create/src/create-nx-workspace.test.ts index cc1e75811b663f..e28e473cc33c4b 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,21 @@ describe('create-nx-workspace', () => { checkFilesExist(packageManagerLockFile['pnpm']); checkFilesDoNotExist( packageManagerLockFile['yarn'], - packageManagerLockFile['npm'] + packageManagerLockFile['npm'], + packageManagerLockFile['bun'] + ); + process.env.SELECTED_PM = packageManager; + }, 90000); + } + + if (packageManager === 'bun') { + it('should use bun when invoked with bunx', () => { + setupProject('bun'); + checkFilesExist(packageManagerLockFile['bun']); + checkFilesDoNotExist( + packageManagerLockFile['yarn'], + packageManagerLockFile['npm'], + packageManagerLockFile['pnpm'] ); process.env.SELECTED_PM = packageManager; }, 90000); @@ -414,7 +429,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/nx/ab-testing.ts b/packages/create-nx-workspace/src/utils/nx/ab-testing.ts index a5f06fdd242b0f..e2cadd27de3df1 100644 --- a/packages/create-nx-workspace/src/utils/nx/ab-testing.ts +++ b/packages/create-nx-workspace/src/utils/nx/ab-testing.ts @@ -119,6 +119,11 @@ export async function recordStat(opts: { function shouldRecordStats(): boolean { const pmc = getPackageManagerCommand(); + if (!pmc.getRegistryUrl) { + // Fallback on true as Package management doesn't support reading config for registry. + // currently Bun doesn't support fetching config settings https://github.com/oven-sh/bun/issues/7140 + return true; + } try { const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' }); const url = new URL(stdout.trim()); diff --git a/packages/create-nx-workspace/src/utils/package-manager.ts b/packages/create-nx-workspace/src/utils/package-manager.ts index 6aae339376c32b..cf4aecd118ff69 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' @@ -38,7 +40,8 @@ export function getPackageManagerCommand( exec: string; preInstall?: string; globalAdd: string; - getRegistryUrl: string; + // Make this required once bun adds programatically support for reading config https://github.com/oven-sh/bun/issues/7140 + getRegistryUrl?: string; } { const pmVersion = getPackageManagerVersion(packageManager); const [pmMajor, pmMinor] = pmVersion.split('.'); @@ -79,6 +82,13 @@ export function getPackageManagerCommand( globalAdd: 'npm i -g', getRegistryUrl: 'npm config get registry', }; + case 'bun': + // bun doesn't current support programatically reading config https://github.com/oven-sh/bun/issues/7140 + return { + install: 'bun install --silent --ignore-scripts', + 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 b3a806f1f34901..f48060e133254f 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 251d958cd1b7ff..73b7c2360747b9 100644 --- a/packages/nx/src/command-line/init/implementation/react/index.ts +++ b/packages/nx/src/command-line/init/implementation/react/index.ts @@ -243,6 +243,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 74409efd7249c5..9f4d2e58739186 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(`bun ${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(`bun ${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 f2b1d162b7ee61..4575cf83b4cfeb 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/ab-testing.ts b/packages/nx/src/utils/ab-testing.ts index 09ea28ff7d3086..9966fe056e579d 100644 --- a/packages/nx/src/utils/ab-testing.ts +++ b/packages/nx/src/utils/ab-testing.ts @@ -101,6 +101,11 @@ export async function recordStat(opts: { function shouldRecordStats(): boolean { const pmc = getPackageManagerCommand(); + if (!pmc.getRegistryUrl) { + // Fallback on true as Package management doesn't support reading config for registry. + // currently Bun doesn't support fetching config settings https://github.com/oven-sh/bun/issues/7140 + return true; + } try { const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' }); const url = new URL(stdout.trim()); diff --git a/packages/nx/src/utils/package-manager.spec.ts b/packages/nx/src/utils/package-manager.spec.ts index 3ba3c7d8acfe1b..cd8f1377a63ba3 100644 --- a/packages/nx/src/utils/package-manager.spec.ts +++ b/packages/nx/src/utils/package-manager.spec.ts @@ -10,6 +10,9 @@ import { describe('package-manager', () => { describe('detectPackageManager', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); it('should detect package manager in nxJson', () => { jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({ cli: { @@ -30,13 +33,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('yarn'); - expect(fs.existsSync).toHaveBeenNthCalledWith(1, 'yarn.lock'); + expect(fs.existsSync).toHaveBeenNthCalledWith(2, 'yarn.lock'); }); it('should detect pnpm package manager from pnpm-lock.yaml', () => { @@ -49,6 +54,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 +65,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(1); + }); + it('should use npm package manager as default', () => { jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({}); jest.spyOn(fs, 'existsSync').mockImplementation((p) => { @@ -68,13 +96,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(3); }); }); diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index 2fe5f28ef47767..4228b357239e8b 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; @@ -27,7 +27,8 @@ export interface PackageManagerCommands { dlx: string; list: string; run: (script: string, args?: string) => string; - getRegistryUrl: string; + // Make this required once bun adds programatically support for reading config https://github.com/oven-sh/bun/issues/7140 + getRegistryUrl?: string; } /** @@ -37,7 +38,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' @@ -154,6 +157,21 @@ export function getPackageManagerCommand( getRegistryUrl: 'npm config get registry', }; }, + bun: () => { + // bun doesn't current support programatically reading config https://github.com/oven-sh/bun/issues/7140 + 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](); @@ -242,7 +260,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) { @@ -267,6 +290,10 @@ export function copyPackageManagerConfigurationFiles( writeFileSync(destinationPath, updated); break; } + case 'bunfig.toml': { + copyFileSync(f, destinationPath); + break; + } } } } @@ -365,12 +392,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'; } @@ -385,13 +416,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/__snapshots__/ci-workflow.spec.ts.snap b/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap index ceb7f7e8e3e2d3..abe1aaa05d6089 100644 --- a/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap +++ b/packages/workspace/src/generators/ci-workflow/__snapshots__/ci-workflow.spec.ts.snap @@ -216,6 +216,249 @@ jobs: " `; +exports[`CI Workflow generator with bun should generate azure CI config 1`] = ` +"name: CI + +trigger: + - main +pr: + - main + +variables: + CI: 'true' + \${{ if eq(variables['Build.Reason'], 'PullRequest') }}: + NX_BRANCH: $(System.PullRequest.PullRequestNumber) + TARGET_BRANCH: $[replace(variables['System.PullRequest.TargetBranch'],'refs/heads/','origin/')] + BASE_SHA: $(git merge-base $(TARGET_BRANCH) HEAD) + \${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + NX_BRANCH: $(Build.SourceBranchName) + BASE_SHA: $(git rev-parse HEAD~1) + HEAD_SHA: $(git rev-parse HEAD) + +jobs: + - job: main + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: self + fetchDepth: 0 + # Set Azure Devops CLI default settings + - bash: az devops configure --defaults organization=$(System.TeamFoundationCollectionUri) project=$(System.TeamProject) + displayName: 'Set default Azure DevOps organization and project' + # Get last successfull commit from Azure Devops CLI + - displayName: 'Get last successful commit SHA' + condition: ne(variables['Build.Reason'], 'PullRequest') + env: + AZURE_DEVOPS_EXT_PAT: $(System.AccessToken) + bash: | + LAST_SHA=$(az pipelines build list --branch $(Build.SourceBranchName) --definition-ids $(System.DefinitionId) --result succeeded --top 1 --query "[0].triggerInfo.\\"ci.sourceSha\\"") + if [ -z "$LAST_SHA" ] + then + echo "Last successful commit not found. Using fallback 'HEAD~1': $BASE_SHA" + else + echo "Last successful commit SHA: $LAST_SHA" + echo "##vso[task.setvariable variable=BASE_SHA]$LAST_SHA" + fi + + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - script: bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + - script: bun install --no-cache + - script: git branch --track main origin/main + condition: eq(variables['Build.Reason'], 'PullRequest') + + - script: bun nx-cloud record -- nx format:check --base=$(BASE_SHA) --head=$(HEAD_SHA) + - script: bun nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) -t lint test build +" +`; + +exports[`CI Workflow generator with bun should generate bitbucket pipelines config 1`] = ` +"image: node:20 + +clone: + depth: full + +pipelines: + pull-requests: + '**': + - step: + name: 'Build and test affected apps on Pull Requests' + script: + - export NX_BRANCH=$BITBUCKET_PR_ID + + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + - bun install --no-cache + + - bun nx-cloud record -- nx format:check + - bun nx affected -t lint test build --base=origin/main + + branches: + main: + - step: + name: 'Build and test affected apps on "main" branch changes' + script: + - export NX_BRANCH=$BITBUCKET_BRANCH + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + - bun install --no-cache + + - bun nx-cloud record -- nx format:check + - bun nx affected -t lint test build --base=HEAD~1 +" +`; + +exports[`CI Workflow generator with bun should generate circleci CI config 1`] = ` +"version: 2.1 + +orbs: + nx: nrwl/nx@1.6.2 + +jobs: + main: + docker: + - image: cimg/node:lts-browsers + steps: + - checkout + + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - run: bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + - run: bun install --no-cache + - nx/set-shas: + main-branch-name: 'main' + + # Required for nx affected if we're on a branch + - run: git branch --track main origin/main + condition: eq(variables['Build.Reason'], 'PullRequest') + + - run: bun nx-cloud record -- nx format:check --base=$NX_BASE --head=$NX_HEAD + - run: bun nx affected --base=$NX_BASE --head=$NX_HEAD -t lint test build + +workflows: + version: 2 + + ci: + jobs: + - main +" +`; + +exports[`CI Workflow generator with bun should generate github CI config 1`] = ` +"name: CI + +on: + push: + branches: + - main + pull_request: + +permissions: + actions: read + contents: read + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - run: bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + # Cache node_modules + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: 'bun' + - run: bun install --no-cache + - uses: nrwl/nx-set-shas@v4 + + - run: git branch --track main origin/main + if: \${{ github.event_name == 'pull_request' }} + + - run: bun nx-cloud record -- nx format:check + - run: bun nx affected -t lint test build +" +`; + +exports[`CI Workflow generator with bun should generate github CI config with custom name 1`] = ` +"name: My custom-workflow + +on: + push: + branches: + - main + pull_request: + +permissions: + actions: read + contents: read + +jobs: + main: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - run: bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + # Cache node_modules + - uses: actions/setup-node@v3 + with: + node-version: 20 + cache: 'bun' + - run: bun install --no-cache + - uses: nrwl/nx-set-shas@v4 + + - run: git branch --track main origin/main + if: \${{ github.event_name == 'pull_request' }} + + - run: bun nx-cloud record -- nx format:check + - run: bun nx affected -t lint test build +" +`; + +exports[`CI Workflow generator with bun should generate gitlab config 1`] = ` +"image: node:20 +variables: + CI: 'true' + +# Main job +CI: + interruptible: true + only: + - main + - merge_requests + script: + # Connect your workspace on my.nx.app and uncomment this to enable task distribution. + # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "build" targets have been requested + # - bun nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build" + + - bun install --no-cache + - NX_HEAD=$CI_COMMIT_SHA + - NX_BASE=\${CI_MERGE_REQUEST_DIFF_BASE_SHA:-$CI_COMMIT_BEFORE_SHA} + + - bun nx-cloud record -- nx format:check --base=$NX_BASE --head=$NX_HEAD + - bun nx affected --base=$NX_BASE --head=$NX_HEAD -t lint test build +" +`; + +exports[`CI Workflow generator with bun should throw error is nx cloud is not set 1`] = `"This workspace is not connected to Nx Cloud."`; + exports[`CI Workflow generator with npm should generate azure CI config 1`] = ` "name: CI 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 f421fa2b66fabc..5986e077ddb6b8 100644 --- a/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts +++ b/packages/workspace/src/generators/ci-workflow/ci-workflow.spec.ts @@ -21,7 +21,7 @@ jest.mock('fs', () => { return { ...jest.requireActual('fs'), existsSync: (p) => - p.endsWith('yarn.lock') || p.endsWith('pnpm-lock.yaml') + p.endsWith('yarn.lock') || p.endsWith('pnpm-lock.yaml') || p.endsWith('bun.lockb') ? memFs.existsSync(p) : actualFs.existsSync(p), }; @@ -38,11 +38,13 @@ 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; - if (packageManager === 'yarn') { + if (packageManager === 'bun') { + fileSys = { 'bun.lockb': '' }; + } else if (packageManager === 'yarn') { fileSys = { 'yarn.lock': '' }; } else if (packageManager === 'pnpm') { fileSys = { 'pnpm-lock.yaml': '' }; diff --git a/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ index 6466babf427fba..1e6a205fe8f798 100644 --- a/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ +++ b/packages/workspace/src/generators/ci-workflow/files/azure/azure-pipelines.yml__tmpl__ @@ -45,6 +45,11 @@ jobs: - script: npm install --prefix=$HOME/.local -g pnpm@8 displayName: Install PNPM + <% } %> + <% if(packageManager == 'bun'){ %> + - script: npm install --prefix=$HOME/.local -g Bun + displayName: Install Bun + <% } %> # Connect your workspace on <%= nxCloudHost %> and uncomment this to enable task distribution. # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" targets have been requested diff --git a/packages/workspace/src/generators/ci-workflow/files/bitbucket-pipelines/bitbucket-pipelines.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/bitbucket-pipelines/bitbucket-pipelines.yml__tmpl__ index daa1690e199345..bc6df793a4e579 100644 --- a/packages/workspace/src/generators/ci-workflow/files/bitbucket-pipelines/bitbucket-pipelines.yml__tmpl__ +++ b/packages/workspace/src/generators/ci-workflow/files/bitbucket-pipelines/bitbucket-pipelines.yml__tmpl__ @@ -14,6 +14,10 @@ pipelines: <% if(packageManager == 'pnpm'){ %> - npm install --prefix=$HOME/.local -g pnpm@8 + <% } %> + <% if(packageManager == 'bun'){ %> + - npm install --prefix=$HOME/.local -g bun + <% } %> # Connect your workspace on <%= nxCloudHost %> and uncomment this to enable task distribution. # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" targets have been requested @@ -38,6 +42,9 @@ pipelines: <% if(packageManager == 'pnpm'){ %> - npm install --prefix=$HOME/.local -g pnpm@8 <% } %> + <% if(packageManager == 'bun'){ %> + - npm install --prefix=$HOME/.local -g bun + <% } %> - <%= packageManagerInstall %> # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud diff --git a/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ index 84f5cba7961da2..8d99f60dfb1267 100644 --- a/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ +++ b/packages/workspace/src/generators/ci-workflow/files/circleci/.circleci/config.yml__tmpl__ @@ -14,6 +14,12 @@ jobs: name: Install PNPM command: npm install --prefix=$HOME/.local -g pnpm@8 <% } %> + <% if(packageManager == 'bun'){ %> + - run: + name: Install Bun + command: npm install --prefix=$HOME/.local -g bun + <% } %> + # Connect your workspace on <%= nxCloudHost %> and uncomment this to enable task distribution. # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" targets have been requested diff --git a/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ index 36eeb588f503ff..be73dff3c3901c 100644 --- a/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ +++ b/packages/workspace/src/generators/ci-workflow/files/github/.github/workflows/__workflowFileName__.yml__tmpl__ @@ -22,16 +22,23 @@ jobs: with: version: 8 <% } %> + <% if(packageManager == 'bun'){ %> + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + <% } %> # Connect your workspace on <%= nxCloudHost %> and uncomment this to enable task distribution. # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" targets have been requested # - run: <%= packageManagerPreInstallPrefix %> nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" + <% if(packageManager != 'bun'){ %> # Cache node_modules - uses: actions/setup-node@v3 with: node-version: 20 cache: '<%= packageManager %>' + <% } %> - run: <%= packageManagerInstall %> - uses: nrwl/nx-set-shas@v4 diff --git a/packages/workspace/src/generators/ci-workflow/files/gitlab/.gitlab-ci.yml__tmpl__ b/packages/workspace/src/generators/ci-workflow/files/gitlab/.gitlab-ci.yml__tmpl__ index fa6edae8017ea6..ebd364670cd0de 100644 --- a/packages/workspace/src/generators/ci-workflow/files/gitlab/.gitlab-ci.yml__tmpl__ +++ b/packages/workspace/src/generators/ci-workflow/files/gitlab/.gitlab-ci.yml__tmpl__ @@ -12,6 +12,9 @@ variables: <% if(packageManager == 'pnpm'){ %> - npm install --prefix=$HOME/.local -g pnpm@8 <% } %> + <% if(packageManager == 'bun'){ %> + - npm install --prefix=$HOME/.local -g bun + <% } %> # Connect your workspace on <%= nxCloudHost %> and uncomment this to enable task distribution. # The "--stop-agents-after" is optional, but allows idle agents to shut down once the "<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" targets have been requested # - <%= packageManagerPreInstallPrefix %> nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>" diff --git a/packages/workspace/src/generators/new/files-package-based-repo/packages/__dot__gitkeep b/packages/workspace/src/generators/new/files-package-based-repo/packages/__dot__gitkeep new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/packages/workspace/src/generators/new/schema.json b/packages/workspace/src/generators/new/schema.json index a8ce0448a72ba2..16ba7d59b2c912 100644 --- a/packages/workspace/src/generators/new/schema.json +++ b/packages/workspace/src/generators/new/schema.json @@ -56,7 +56,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "framework": { "description": "The framework which the application is using", diff --git a/packages/workspace/src/generators/preset/schema.json b/packages/workspace/src/generators/preset/schema.json index 6757f69b1db3c5..891e099699022a 100644 --- a/packages/workspace/src/generators/preset/schema.json +++ b/packages/workspace/src/generators/preset/schema.json @@ -62,7 +62,7 @@ "packageManager": { "description": "The package manager used to install dependencies.", "type": "string", - "enum": ["npm", "yarn", "pnpm"] + "enum": ["npm", "yarn", "pnpm", "bun"] }, "framework": { "description": "The framework which the application is using", diff --git a/scripts/check-lock-files.js b/scripts/check-lock-files.js index 203505c78a8fb6..e4584d82d54ea3 100644 --- a/scripts/check-lock-files.js +++ b/scripts/check-lock-files.js @@ -7,6 +7,11 @@ function checkLockFiles() { 'Invalid occurence of "package-lock.json" file. Please remove it and use only "pnpm-lock.yaml"' ); } + if (fs.existsSync('bun.lockb')) { + errors.push( + 'Invalid occurence of "bun.lockb" file. Please remove it and use only "pnpm-lock.yaml"' + ); + } if (fs.existsSync('yarn.lock')) { errors.push( 'Invalid occurence of "yarn.lock" file. Please remove it and use only "pnpm-lock.yaml"'