feat(core): add bun package manager (#22602)

Bun uses yarn lock for it's binary file. Running the binary will produce
the content of a yarn lock file (v1)

Other option is to use the -y command on add and install. This will
create a yarn lock file and then createLockFile can just modify the
yarn.lock file instead?

This is the PR made from #19113 and pushed due to #22402 being closed.

PS Bun feels more stable since the PR was first created!

This PR will resolve #22283 and start of #21075
This commit is contained in:
Jordan Hall 2024-05-22 21:51:21 +01:00 committed by GitHub
parent 383be1f7d4
commit 80702b59c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 490 additions and 35 deletions

View File

@ -129,7 +129,7 @@ Do you want Nx Cloud to make your CI fast?
Type: `string` Type: `string`
Choices: [npm, pnpm, yarn] Choices: [bun, npm, pnpm, yarn]
Default: `npm` Default: `npm`

View File

@ -1,3 +1,3 @@
# Type alias: PackageManager # Type alias: PackageManager
Ƭ **PackageManager**: `"yarn"` \| `"pnpm"` \| `"npm"` Ƭ **PackageManager**: `"yarn"` \| `"pnpm"` \| `"npm"` \| `"bun"`

View File

@ -129,7 +129,7 @@ Do you want Nx Cloud to make your CI fast?
Type: `string` Type: `string`
Choices: [npm, pnpm, yarn] Choices: [bun, npm, pnpm, yarn]
Default: `npm` Default: `npm`

View File

@ -53,7 +53,7 @@
"packageManager": { "packageManager": {
"description": "The package manager used to install dependencies.", "description": "The package manager used to install dependencies.",
"type": "string", "type": "string",
"enum": ["npm", "yarn", "pnpm"] "enum": ["npm", "yarn", "pnpm", "bun"]
}, },
"framework": { "framework": {
"description": "The framework which the application is using", "description": "The framework which the application is using",

View File

@ -59,7 +59,7 @@
"packageManager": { "packageManager": {
"description": "The package manager used to install dependencies.", "description": "The package manager used to install dependencies.",
"type": "string", "type": "string",
"enum": ["npm", "yarn", "pnpm"] "enum": ["npm", "yarn", "pnpm", "bun"]
}, },
"framework": { "framework": {
"description": "The framework which the application is using", "description": "The framework which the application is using",

View File

@ -171,6 +171,19 @@ export function getPackageManagerCommand({
list: 'pnpm ls --depth 10', list: 'pnpm ls --depth 10',
runLerna: `pnpm exec lerna`, 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]; }[packageManager.trim() as PackageManager];
} }

View File

@ -78,7 +78,7 @@ export function newProject({
packages, packages,
}: { }: {
name?: string; name?: string;
packageManager?: 'npm' | 'yarn' | 'pnpm'; packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
unsetProjectNameAndRootFormat?: boolean; unsetProjectNameAndRootFormat?: boolean;
readonly packages?: Array<NxPackage>; readonly packages?: Array<NxPackage>;
} = {}): string { } = {}): string {
@ -240,7 +240,7 @@ export function runCreateWorkspace(
appName?: string; appName?: string;
style?: string; style?: string;
base?: string; base?: string;
packageManager?: 'npm' | 'yarn' | 'pnpm'; packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
extraArgs?: string; extraArgs?: string;
useDetectedPm?: boolean; useDetectedPm?: boolean;
cwd?: string; cwd?: string;
@ -358,7 +358,7 @@ export function runCreatePlugin(
extraArgs, extraArgs,
useDetectedPm = false, useDetectedPm = false,
}: { }: {
packageManager?: 'npm' | 'yarn' | 'pnpm'; packageManager?: 'npm' | 'yarn' | 'pnpm' | 'bun';
extraArgs?: string; extraArgs?: string;
useDetectedPm?: boolean; useDetectedPm?: boolean;
} }
@ -543,6 +543,11 @@ export function newLernaWorkspace({
...json.resolutions, ...json.resolutions,
...overrides, ...overrides,
}; };
} else if (packageManager === 'bun') {
json.overrides = {
...json.resolutions,
...overrides,
};
} else { } else {
json.overrides = overrides; json.overrides = overrides;
} }

View File

@ -23,7 +23,9 @@ export function getPublishedVersion(): string {
} }
export function detectPackageManager(dir: string = ''): PackageManager { 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' ? 'yarn'
: existsSync(join(dir, 'pnpm-lock.yaml')) || : existsSync(join(dir, 'pnpm-lock.yaml')) ||
existsSync(join(dir, 'pnpm-workspace.yaml')) existsSync(join(dir, 'pnpm-workspace.yaml'))
@ -64,8 +66,8 @@ export function isVerboseE2ERun() {
export const e2eCwd = `${e2eRoot}/nx`; export const e2eCwd = `${e2eRoot}/nx`;
export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' { export function getSelectedPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' {
return (process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm') || 'npm'; return (process.env.SELECTED_PM as 'npm' | 'yarn' | 'pnpm' | 'bun') || 'npm';
} }
export function getNpmMajorVersion(): string | undefined { export function getNpmMajorVersion(): string | undefined {
@ -108,6 +110,7 @@ export const packageManagerLockFile = {
npm: 'package-lock.json', npm: 'package-lock.json',
yarn: 'yarn.lock', yarn: 'yarn.lock',
pnpm: 'pnpm-lock.yaml', pnpm: 'pnpm-lock.yaml',
bun: 'bun.lockb',
}; };
export function ensureCypressInstallation() { export function ensureCypressInstallation() {

View File

@ -374,7 +374,7 @@ describe('create-nx-workspace', () => {
}); });
describe('Use detected package manager', () => { describe('Use detected package manager', () => {
function setupProject(envPm: 'npm' | 'yarn' | 'pnpm') { function setupProject(envPm: 'npm' | 'yarn' | 'pnpm' | 'bun') {
process.env.SELECTED_PM = envPm; process.env.SELECTED_PM = envPm;
runCreateWorkspace(uniq('pm'), { runCreateWorkspace(uniq('pm'), {
preset: 'apps', preset: 'apps',
@ -389,7 +389,8 @@ describe('create-nx-workspace', () => {
checkFilesExist(packageManagerLockFile['npm']); checkFilesExist(packageManagerLockFile['npm']);
checkFilesDoNotExist( checkFilesDoNotExist(
packageManagerLockFile['yarn'], packageManagerLockFile['yarn'],
packageManagerLockFile['pnpm'] packageManagerLockFile['pnpm'],
packageManagerLockFile['bun']
); );
process.env.SELECTED_PM = packageManager; process.env.SELECTED_PM = packageManager;
}, 90000); }, 90000);
@ -401,7 +402,21 @@ describe('create-nx-workspace', () => {
checkFilesExist(packageManagerLockFile['pnpm']); checkFilesExist(packageManagerLockFile['pnpm']);
checkFilesDoNotExist( checkFilesDoNotExist(
packageManagerLockFile['yarn'], 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; process.env.SELECTED_PM = packageManager;
}, 90000); }, 90000);
@ -414,7 +429,8 @@ describe('create-nx-workspace', () => {
checkFilesExist(packageManagerLockFile['yarn']); checkFilesExist(packageManagerLockFile['yarn']);
checkFilesDoNotExist( checkFilesDoNotExist(
packageManagerLockFile['pnpm'], packageManagerLockFile['pnpm'],
packageManagerLockFile['npm'] packageManagerLockFile['npm'],
packageManagerLockFile['bun']
); );
process.env.SELECTED_PM = packageManager; process.env.SELECTED_PM = packageManager;
}, 90000); }, 90000);

View File

@ -110,6 +110,7 @@ export async function determinePackageManager(
{ name: 'npm', message: 'NPM' }, { name: 'npm', message: 'NPM' },
{ name: 'yarn', message: 'Yarn' }, { name: 'yarn', message: 'Yarn' },
{ name: 'pnpm', message: 'PNPM' }, { name: 'pnpm', message: 'PNPM' },
{ name: 'bun', message: 'Bun' },
], ],
}, },
]) ])

View File

@ -119,6 +119,11 @@ export async function recordStat(opts: {
function shouldRecordStats(): boolean { function shouldRecordStats(): boolean {
const pmc = getPackageManagerCommand(); 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 { try {
const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' }); const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' });
const url = new URL(stdout.trim()); const url = new URL(stdout.trim());

View File

@ -7,12 +7,14 @@ import { join } from 'path';
* we duplicate the helper functions from @nx/workspace in this file. * 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 type PackageManager = typeof packageManagerList[number];
export function detectPackageManager(dir: string = ''): PackageManager { 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' ? 'yarn'
: existsSync(join(dir, 'pnpm-lock.yaml')) : existsSync(join(dir, 'pnpm-lock.yaml'))
? 'pnpm' ? 'pnpm'
@ -38,7 +40,8 @@ export function getPackageManagerCommand(
exec: string; exec: string;
preInstall?: string; preInstall?: string;
globalAdd: 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 pmVersion = getPackageManagerVersion(packageManager);
const [pmMajor, pmMinor] = pmVersion.split('.'); const [pmMajor, pmMinor] = pmVersion.split('.');
@ -79,6 +82,13 @@ export function getPackageManagerCommand(
globalAdd: 'npm i -g', globalAdd: 'npm i -g',
getRegistryUrl: 'npm config get registry', 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',
};
} }
} }

View File

@ -14,6 +14,7 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
npm: 'package-lock.json', npm: 'package-lock.json',
yarn: 'yarn.lock', yarn: 'yarn.lock',
pnpm: 'pnpm-lock.yaml', pnpm: 'pnpm-lock.yaml',
bun: 'bun.lockb',
}; };
const packageManager = detectPackageManager(host.root); const packageManager = detectPackageManager(host.root);
const packageLockFile = packageManagerLockFile[packageManager]; const packageLockFile = packageManagerLockFile[packageManager];

View File

@ -19,6 +19,7 @@ export default function update(tree: Tree) {
npm: 'package-lock.json', npm: 'package-lock.json',
yarn: 'yarn.lock', yarn: 'yarn.lock',
pnpm: 'pnpm-lock.yaml', pnpm: 'pnpm-lock.yaml',
bun: 'bun.lockb',
}; };
for (const [name, config] of projects.entries()) { for (const [name, config] of projects.entries()) {

View File

@ -366,7 +366,7 @@
"packageManager": { "packageManager": {
"type": "string", "type": "string",
"description": "The default package manager to use.", "description": "The default package manager to use.",
"enum": ["yarn", "pnpm", "npm"] "enum": ["yarn", "pnpm", "npm", "bun"]
} }
} }
}, },

View File

@ -243,6 +243,7 @@ function moveFilesToTempWorkspace(options: NormalizedOptions) {
options.packageManager === 'yarn' ? 'yarn.lock' : null, options.packageManager === 'yarn' ? 'yarn.lock' : null,
options.packageManager === 'pnpm' ? 'pnpm-lock.yaml' : null, options.packageManager === 'pnpm' ? 'pnpm-lock.yaml' : null,
options.packageManager === 'npm' ? 'package-lock.json' : null, options.packageManager === 'npm' ? 'package-lock.json' : null,
options.packageManager === 'bun' ? 'bun.lockb' : null,
]; ];
const optionalCraFiles = ['README.md']; const optionalCraFiles = ['README.md'];

View File

@ -24,6 +24,7 @@ import { hashArray } from '../../hasher/file-hasher';
import { detectPackageManager } from '../../utils/package-manager'; import { detectPackageManager } from '../../utils/package-manager';
import { workspaceRoot } from '../../utils/workspace-root'; import { workspaceRoot } from '../../utils/workspace-root';
import { nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
import { execSync } from 'child_process';
export const name = 'nx/js/dependencies-and-lockfile'; export const name = 'nx/js/dependencies-and-lockfile';
@ -51,7 +52,10 @@ export const createNodes: CreateNodes = [
} }
const lockFilePath = join(workspaceRoot, lockFile); 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); const lockFileHash = getLockFileHash(lockFileContents);
if (!lockFileNeedsReprocessing(lockFileHash)) { if (!lockFileNeedsReprocessing(lockFileHash)) {
@ -91,7 +95,10 @@ export const createDependencies: CreateDependencies = (
parsedLockFile.externalNodes parsedLockFile.externalNodes
) { ) {
const lockFilePath = join(workspaceRoot, getLockFileName(packageManager)); 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); const lockFileHash = getLockFileHash(lockFileContents);
if (!lockFileNeedsReprocessing(lockFileHash)) { if (!lockFileNeedsReprocessing(lockFileHash)) {

View File

@ -45,11 +45,18 @@ import {
const YARN_LOCK_FILE = 'yarn.lock'; const YARN_LOCK_FILE = 'yarn.lock';
const NPM_LOCK_FILE = 'package-lock.json'; const NPM_LOCK_FILE = 'package-lock.json';
const PNPM_LOCK_FILE = 'pnpm-lock.yaml'; 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 YARN_LOCK_PATH = join(workspaceRoot, YARN_LOCK_FILE);
const NPM_LOCK_PATH = join(workspaceRoot, NPM_LOCK_FILE); const NPM_LOCK_PATH = join(workspaceRoot, NPM_LOCK_FILE);
const PNPM_LOCK_PATH = join(workspaceRoot, PNPM_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} * Parses lock file and maps dependencies and metadata to {@link LockFileGraph}
@ -73,6 +80,11 @@ export function getLockFileNodes(
if (packageManager === 'npm') { if (packageManager === 'npm') {
return getNpmLockfileNodes(contents, lockFileHash); 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) { } catch (e) {
if (!isPostInstallProcess()) { if (!isPostInstallProcess()) {
output.error({ output.error({
@ -104,6 +116,10 @@ export function getLockFileDependencies(
if (packageManager === 'npm') { if (packageManager === 'npm') {
return getNpmLockfileDependencies(contents, lockFileHash, context); return getNpmLockfileDependencies(contents, lockFileHash, context);
} }
if (packageManager === 'bun') {
// bun uses yarn v1 for the file format
return getYarnLockfileDependencies(contents, lockFileHash, context);
}
} catch (e) { } catch (e) {
if (!isPostInstallProcess()) { if (!isPostInstallProcess()) {
output.error({ output.error({
@ -126,6 +142,9 @@ export function lockFileExists(packageManager: PackageManager): boolean {
if (packageManager === 'npm') { if (packageManager === 'npm') {
return existsSync(NPM_LOCK_PATH); return existsSync(NPM_LOCK_PATH);
} }
if (packageManager === 'bun') {
return existsSync(BUN_LOCK_PATH);
}
throw new Error( throw new Error(
`Unknown package manager ${packageManager} or lock file missing` `Unknown package manager ${packageManager} or lock file missing`
); );
@ -146,6 +165,9 @@ export function getLockFileName(packageManager: PackageManager): string {
if (packageManager === 'npm') { if (packageManager === 'npm') {
return NPM_LOCK_FILE; return NPM_LOCK_FILE;
} }
if (packageManager === 'bun') {
return BUN_LOCK_FILE;
}
throw new Error(`Unknown package manager: ${packageManager}`); throw new Error(`Unknown package manager: ${packageManager}`);
} }
@ -159,6 +181,9 @@ function getLockFilePath(packageManager: PackageManager): string {
if (packageManager === 'npm') { if (packageManager === 'npm') {
return NPM_LOCK_PATH; return NPM_LOCK_PATH;
} }
if (packageManager === 'bun') {
return BUN_LOCK_PATH;
}
throw new Error(`Unknown package manager: ${packageManager}`); throw new Error(`Unknown package manager: ${packageManager}`);
} }
@ -191,6 +216,12 @@ export function createLockFile(
const prunedGraph = pruneProjectGraph(graph, packageJson); const prunedGraph = pruneProjectGraph(graph, packageJson);
return stringifyNpmLockfile(prunedGraph, content, normalizedPackageJson); 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) { } catch (e) {
if (!isPostInstallProcess()) { if (!isPostInstallProcess()) {
const additionalInfo = [ const additionalInfo = [

View File

@ -101,6 +101,11 @@ export async function recordStat(opts: {
function shouldRecordStats(): boolean { function shouldRecordStats(): boolean {
const pmc = getPackageManagerCommand(); 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 { try {
const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' }); const stdout = execSync(pmc.getRegistryUrl, { encoding: 'utf-8' });
const url = new URL(stdout.trim()); const url = new URL(stdout.trim());

View File

@ -10,6 +10,9 @@ import {
describe('package-manager', () => { describe('package-manager', () => {
describe('detectPackageManager', () => { describe('detectPackageManager', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('should detect package manager in nxJson', () => { it('should detect package manager in nxJson', () => {
jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({ jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({
cli: { cli: {
@ -30,13 +33,15 @@ describe('package-manager', () => {
return false; return false;
case 'package-lock.json': case 'package-lock.json':
return false; return false;
case 'bun.lockb':
return false;
default: default:
return jest.requireActual('fs').existsSync(p); return jest.requireActual('fs').existsSync(p);
} }
}); });
const packageManager = detectPackageManager(); const packageManager = detectPackageManager();
expect(packageManager).toEqual('yarn'); 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', () => { it('should detect pnpm package manager from pnpm-lock.yaml', () => {
@ -49,6 +54,8 @@ describe('package-manager', () => {
return true; return true;
case 'package-lock.json': case 'package-lock.json':
return false; return false;
case 'bun.lockb':
return false;
default: default:
return jest.requireActual('fs').existsSync(p); return jest.requireActual('fs').existsSync(p);
} }
@ -58,6 +65,27 @@ describe('package-manager', () => {
expect(fs.existsSync).toHaveBeenCalledTimes(3); 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', () => { it('should use npm package manager as default', () => {
jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({}); jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({});
jest.spyOn(fs, 'existsSync').mockImplementation((p) => { jest.spyOn(fs, 'existsSync').mockImplementation((p) => {
@ -68,13 +96,15 @@ describe('package-manager', () => {
return false; return false;
case 'package-lock.json': case 'package-lock.json':
return false; return false;
case 'bun.lockb':
return false;
default: default:
return jest.requireActual('fs').existsSync(p); return jest.requireActual('fs').existsSync(p);
} }
}); });
const packageManager = detectPackageManager(); const packageManager = detectPackageManager();
expect(packageManager).toEqual('npm'); expect(packageManager).toEqual('npm');
expect(fs.existsSync).toHaveBeenCalledTimes(5); expect(fs.existsSync).toHaveBeenCalledTimes(3);
}); });
}); });

View File

@ -13,7 +13,7 @@ import { workspaceRoot } from './workspace-root';
const execAsync = promisify(exec); const execAsync = promisify(exec);
export type PackageManager = 'yarn' | 'pnpm' | 'npm'; export type PackageManager = 'yarn' | 'pnpm' | 'npm' | 'bun';
export interface PackageManagerCommands { export interface PackageManagerCommands {
preInstall?: string; preInstall?: string;
@ -27,7 +27,8 @@ export interface PackageManagerCommands {
dlx: string; dlx: string;
list: string; list: string;
run: (script: string, args?: string) => 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(); const nxJson = readNxJson();
return ( return (
nxJson.cli?.packageManager ?? nxJson.cli?.packageManager ??
(existsSync(join(dir, 'yarn.lock')) (existsSync(join(dir, 'bun.lockb'))
? 'bun'
: existsSync(join(dir, 'yarn.lock'))
? 'yarn' ? 'yarn'
: existsSync(join(dir, 'pnpm-lock.yaml')) : existsSync(join(dir, 'pnpm-lock.yaml'))
? 'pnpm' ? 'pnpm'
@ -154,6 +157,21 @@ export function getPackageManagerCommand(
getRegistryUrl: 'npm config get registry', 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](); return commands[packageManager]();
@ -242,7 +260,12 @@ export function copyPackageManagerConfigurationFiles(
root: string, root: string,
destination: 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}. // f is an absolute path, including the {workspaceRoot}.
const f = findFileInPackageJsonDirectory(packageManagerConfigFile, root); const f = findFileInPackageJsonDirectory(packageManagerConfigFile, root);
if (f) { if (f) {
@ -267,6 +290,10 @@ export function copyPackageManagerConfigurationFiles(
writeFileSync(destinationPath, updated); writeFileSync(destinationPath, updated);
break; break;
} }
case 'bunfig.toml': {
copyFileSync(f, destinationPath);
break;
}
} }
} }
} }
@ -365,12 +392,16 @@ export async function packageRegistryView(
args: string args: string
): Promise<string> { ): Promise<string> {
let pm = detectPackageManager(); let pm = detectPackageManager();
if (pm === 'yarn') { if (pm === 'yarn' || pm === 'bun') {
/** /**
* yarn has `yarn info` but it behaves differently than (p)npm, * yarn has `yarn info` but it behaves differently than (p)npm,
* which makes it's usage unreliable * which makes it's usage unreliable
* *
* @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 * @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 <package-name>"
*/ */
pm = 'npm'; pm = 'npm';
} }
@ -385,13 +416,15 @@ export async function packageRegistryPack(
version: string version: string
): Promise<{ tarballPath: string }> { ): Promise<{ tarballPath: string }> {
let pm = detectPackageManager(); let pm = detectPackageManager();
if (pm === 'yarn') { if (pm === 'yarn' || pm === 'bun') {
/** /**
* `(p)npm pack` will download a tarball of the specified version, * `(p)npm pack` will download a tarball of the specified version,
* whereas `yarn` pack creates a tarball of the active workspace, so it * whereas `yarn` pack creates a tarball of the active workspace, so it
* does not work for getting the content of a library. * does not work for getting the content of a library.
* *
* @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994 * @see https://github.com/nrwl/nx/pull/9667#discussion_r842553994
*
* bun doesn't currently support pack
*/ */
pm = 'npm'; pm = 'npm';
} }

View File

@ -166,6 +166,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -206,6 +207,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -216,6 +218,254 @@ 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
- 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
displayName: 'Get last successful commit SHA'
condition: ne(variables['Build.Reason'], 'PullRequest')
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
- script: npm install --prefix=$HOME/.local -g Bun
displayName: Install Bun
# Connect your workspace on 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: bunx 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')
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - script: bun nx-cloud record -- echo Hello World
- script: bun nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) 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
- npm install --prefix=$HOME/.local -g bun
# Connect your workspace on 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
# - bunx 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 --base=origin/main -t lint test build
branches:
main:
- step:
name: 'Build and test affected apps on "main" branch changes'
script:
- export NX_BRANCH=$BITBUCKET_BRANCH
# Connect your workspace on 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
# - bunx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build"
- npm install --prefix=$HOME/.local -g bun
- bun install --no-cache
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - bun nx-cloud record -- echo Hello World
- 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
- run:
name: Install Bun
command: npm install --prefix=$HOME/.local -g bun
# Connect your workspace on 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: bunx 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'
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: bun nx-cloud record -- echo Hello World
- 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
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
# Connect your workspace on 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: bunx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build"
- run: bun install --no-cache
- uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: bun nx-cloud record -- echo Hello World
- 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
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
# Connect your workspace on 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: bunx nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="build"
- run: bun install --no-cache
- uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: bun nx-cloud record -- echo Hello World
- 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:
- npm install --prefix=$HOME/.local -g bun
# Connect your workspace on 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
# - bunx 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}
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - bun nx-cloud record -- echo Hello World
- bun nx affected --base=$NX_BASE --head=$NX_HEAD -t lint test build
"
`;
exports[`CI Workflow generator with npm should generate azure CI config 1`] = ` exports[`CI Workflow generator with npm should generate azure CI config 1`] = `
"name: CI "name: CI
@ -379,6 +629,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -418,6 +669,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'npm' cache: 'npm'
- run: npm ci - run: npm ci
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -631,6 +883,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -674,6 +927,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -874,6 +1128,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'yarn' cache: 'yarn'
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
@ -913,6 +1168,7 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'yarn' cache: 'yarn'
- run: yarn install --frozen-lockfile - run: yarn install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4

View File

@ -21,7 +21,9 @@ jest.mock('fs', () => {
return { return {
...jest.requireActual<any>('fs'), ...jest.requireActual<any>('fs'),
existsSync: (p) => 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) ? memFs.existsSync(p)
: actualFs.existsSync(p), : actualFs.existsSync(p),
}; };
@ -38,11 +40,13 @@ describe('CI Workflow generator', () => {
vol.reset(); vol.reset();
}); });
['npm', 'yarn', 'pnpm'].forEach((packageManager: PackageManager) => { ['npm', 'yarn', 'pnpm', 'bun'].forEach((packageManager: PackageManager) => {
describe(`with ${packageManager}`, () => { describe(`with ${packageManager}`, () => {
beforeEach(() => { beforeEach(() => {
let fileSys; let fileSys;
if (packageManager === 'yarn') { if (packageManager === 'bun') {
fileSys = { 'bun.lockb': '' };
} else if (packageManager === 'yarn') {
fileSys = { 'yarn.lock': '' }; fileSys = { 'yarn.lock': '' };
} else if (packageManager === 'pnpm') { } else if (packageManager === 'pnpm') {
fileSys = { 'pnpm-lock.yaml': '' }; fileSys = { 'pnpm-lock.yaml': '' };

View File

@ -45,6 +45,11 @@ jobs:
- script: npm install --prefix=$HOME/.local -g pnpm@8 - script: npm install --prefix=$HOME/.local -g pnpm@8
displayName: Install PNPM 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. # 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 # 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

View File

@ -14,6 +14,10 @@ pipelines:
<% if(packageManager == 'pnpm'){ %> <% if(packageManager == 'pnpm'){ %>
- npm install --prefix=$HOME/.local -g pnpm@8 - 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. # 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 # 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'){ %> <% if(packageManager == 'pnpm'){ %>
- npm install --prefix=$HOME/.local -g pnpm@8 - npm install --prefix=$HOME/.local -g pnpm@8
<% } %> <% } %>
<% if(packageManager == 'bun'){ %>
- npm install --prefix=$HOME/.local -g bun
<% } %>
- <%= packageManagerInstall %> - <%= packageManagerInstall %>
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud

View File

@ -14,6 +14,12 @@ jobs:
name: Install PNPM name: Install PNPM
command: npm install --prefix=$HOME/.local -g pnpm@8 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. # 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 # 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

View File

@ -22,16 +22,23 @@ jobs:
with: with:
version: 8 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. # 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 # 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<% } %>" # - 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 # Cache node_modules
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 20 node-version: 20
cache: '<%= packageManager %>' cache: '<%= packageManager %>'
<% } %>
- run: <%= packageManagerInstall %> - run: <%= packageManagerInstall %>
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4

View File

@ -12,6 +12,9 @@ variables:
<% if(packageManager == 'pnpm'){ %> <% if(packageManager == 'pnpm'){ %>
- npm install --prefix=$HOME/.local -g pnpm@8 - 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. # 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 # 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<% } %>" # - <%= packageManagerPreInstallPrefix %> nx-cloud start-ci-run --distribute-on="5 linux-medium-js" --stop-agents-after="<% if(hasE2E){ %>e2e-ci<% } else { %>build<% } %>"

View File

@ -56,7 +56,7 @@
"packageManager": { "packageManager": {
"description": "The package manager used to install dependencies.", "description": "The package manager used to install dependencies.",
"type": "string", "type": "string",
"enum": ["npm", "yarn", "pnpm"] "enum": ["npm", "yarn", "pnpm", "bun"]
}, },
"framework": { "framework": {
"description": "The framework which the application is using", "description": "The framework which the application is using",

View File

@ -62,7 +62,7 @@
"packageManager": { "packageManager": {
"description": "The package manager used to install dependencies.", "description": "The package manager used to install dependencies.",
"type": "string", "type": "string",
"enum": ["npm", "yarn", "pnpm"] "enum": ["npm", "yarn", "pnpm", "bun"]
}, },
"framework": { "framework": {
"description": "The framework which the application is using", "description": "The framework which the application is using",

View File

@ -7,6 +7,11 @@ function checkLockFiles() {
'Invalid occurence of "package-lock.json" file. Please remove it and use only "pnpm-lock.yaml"' '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')) { if (fs.existsSync('yarn.lock')) {
errors.push( errors.push(
'Invalid occurence of "yarn.lock" file. Please remove it and use only "pnpm-lock.yaml"' 'Invalid occurence of "yarn.lock" file. Please remove it and use only "pnpm-lock.yaml"'