feat(core): support bun's new text-based lockfile and use bun publish within nx release (#30064)

This commit is contained in:
James Henry 2025-02-19 13:42:35 +04:00 committed by GitHub
parent 1047991200
commit 443d4fd27a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 660 additions and 90 deletions

View File

@ -19,6 +19,26 @@ launch-templates:
key: 'pnpm-lock.yaml' key: 'pnpm-lock.yaml'
paths: .pnpm-store paths: .pnpm-store
base-branch: 'master' base-branch: 'master'
- name: Install zip and unzip
script: sudo apt-get -yqq install zip unzip
# TODO: Remove this once the PR to set it on agents by default is merged
- name: Set SHELL environment variable
script: |
# We need $SHELL to be set for the bun installation to correctly link `bunx`
echo "SHELL=/usr/bin/bash" >> $NX_CLOUD_ENV
- name: Install bun
script: |
curl -fsSL https://bun.sh/install | bash
echo "BUN_INSTALL=$HOME/.bun" >> $NX_CLOUD_ENV
echo "PATH=$HOME/.bun/bin:$PATH" >> $NX_CLOUD_ENV
- name: Check bun
script: |
bun --version
- name: Install e2e deps - name: Install e2e deps
script: | script: |
sudo apt-get update sudo apt-get update
@ -50,8 +70,6 @@ launch-templates:
- name: Load Cargo Env - name: Load Cargo Env
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
- name: Install zip and unzip
script: sudo apt-get -yqq install zip unzip
linux-extra-large: linux-extra-large:
resource-class: 'docker_linux_amd64/extra_large' resource-class: 'docker_linux_amd64/extra_large'
image: 'ubuntu22.04-node20.11-v10' image: 'ubuntu22.04-node20.11-v10'
@ -72,6 +90,26 @@ launch-templates:
key: 'pnpm-lock.yaml' key: 'pnpm-lock.yaml'
paths: .pnpm-store paths: .pnpm-store
base-branch: 'master' base-branch: 'master'
- name: Install zip and unzip
script: sudo apt-get -yqq install zip unzip
# TODO: Remove this once the PR to set it on agents by default is merged
- name: Set SHELL environment variable
script: |
# We need $SHELL to be set for the bun installation to correctly link `bunx`
echo "SHELL=/usr/bin/bash" >> $NX_CLOUD_ENV
- name: Install bun
script: |
curl -fsSL https://bun.sh/install | bash
echo "BUN_INSTALL=$HOME/.bun" >> $NX_CLOUD_ENV
echo "PATH=$HOME/.bun/bin:$PATH" >> $NX_CLOUD_ENV
- name: Check bun
script: |
bun --version
- name: Install e2e deps - name: Install e2e deps
script: | script: |
sudo apt-get update sudo apt-get update
@ -102,6 +140,3 @@ launch-templates:
- name: Load Cargo Env - name: Load Cargo Env
script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV script: echo "PATH=$HOME/.cargo/bin:$PATH" >> $NX_CLOUD_ENV
- name: Install zip and unzip
script: sudo apt-get -yqq install zip unzip

View File

@ -124,13 +124,13 @@ This package.json is now valid and ready to be published to the registry.
## Scenario 3: I want to update package versions directly in my source files, but use local dependency references via file/workspace ## Scenario 3: I want to update package versions directly in my source files, but use local dependency references via file/workspace
{% callout type="caution" title="This scenario is only supported when your package manager is pnpm" %} {% callout type="caution" title="This scenario is currently only supported when your package manager is pnpm or bun" %}
pnpm is the only package manager that provides a publish command that supports dynamically swapping the `file:` and `workspace:*` references with the actual version number at publish time. pnpm and bun are the only package managers that provide a publish command that both supports dynamically swapping the `file:` and `workspace:*` references with the actual version number at publish time, and provides the customization needed for us to wrap it. `yarn npm publish` does support the replacements but is very limited on customization options.
{% /callout %} {% /callout %}
This is a more advanced scenario because it removes the clean separation of concerns between versioning and publishing. The reason for this is that the `file:` and `workspace:` references simply have to be replaced with actual version numbers before they are written to the registry, otherwise they will break when a user tries to install the package. If versioning does not replace them, publishing needs to. This is a more advanced scenario because it removes the clean separation of concerns between versioning and publishing. The reason for this is that the `file:` and `workspace:` references simply have to be replaced with actual version numbers before they are written to the registry, otherwise they will break when a user tries to install the package. If versioning does not replace them, publishing needs to.
As mentioned at the start of this recipe, Nx Release intentionally does not manipulate your packages in memory during publishing, so this scenario is only supported when your package manager provides publishing functionality which dynamically swaps the local references. **Currently this is only supported by pnpm.** As mentioned at the start of this recipe, Nx Release intentionally does not manipulate your packages in memory during publishing, so this scenario is only supported when your package manager provides publishing functionality which dynamically swaps the local references. **Currently this is only supported by pnpm and bun.**
Let's first look at the default behavior of Nx Release, which is to update the all version references in the source package.json files with the new version number. Let's first look at the default behavior of Nx Release, which is to update the all version references in the source package.json files with the new version number.
@ -190,4 +190,4 @@ Now, that same patch release to the source package.json file will result in the
} }
``` ```
Again, this is not in a valid state to be published to the registry, and so the publishing step will need to handle this. **This is only supported by pnpm**, in which case Nx Release invokes `pnpm publish` instead of `npm publish` behind the scenes during publishing, and you will receive a clear error if you attempt to use such a package.json with another package manager. Again, this is not in a valid state to be published to the registry, and so the publishing step will need to handle this. **This is only supported by pnpm and bun**, in which case Nx Release invokes `pnpm publish` or `bun publish` instead of `npm publish` behind the scenes during publishing, and you will receive a clear error if you attempt to use such a package.json with npm or yarn.

View File

@ -0,0 +1,352 @@
import { NxJsonConfiguration } from '@nx/devkit';
import {
cleanupProject,
getPackageManagerCommand,
newProject,
readJson,
runCLI,
runCommandAsync,
tmpProjPath,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { execSync } from 'node:child_process';
import { join } from 'node:path';
expect.addSnapshotSerializer({
serialize(str: string) {
return (
str
// Remove all output unique to specific projects to ensure deterministic snapshots
.replaceAll(/my-pkg-\d+/g, '{project-name}')
.replaceAll(
/integrity:\s*.*/g,
'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
)
.replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}')
.replaceAll(/\d*B index\.js/g, 'XXB index.js')
.replaceAll(/\d*B\s+project\.json/g, 'XXB project.json')
.replaceAll(/\d*B\s+package\.json/g, 'XXXB package.json')
.replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB')
.replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb')
.replaceAll(/\d*B\s+src\//g, 'XXB src/')
.replaceAll(/\d*B\s+index/g, 'XXB index')
.replaceAll(/total files:\s+\d*/g, 'total files: X')
.replaceAll(/\d*B\s+README.md/g, 'XXB README.md')
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
.replaceAll(/(\w+) lock file/g, 'PM lock file')
// Normalize the version title date.
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
// We trim each line to reduce the chances of snapshot flakiness
// Slightly different handling needed for bun (length can be 8)
.replaceAll(/[a-fA-F0-9]{7,8}/g, '{COMMIT_SHA}')
.replaceAll(/bun publish v\d+\.\d+\.\d+/g, 'bun publish vX.X.X')
.replaceAll(
/Integrity:\s*.*/g,
'Integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
)
.split('\n')
.map((r) => r.trim())
.filter(Boolean)
.join('\n')
);
},
test(val: string) {
return val != null && typeof val === 'string';
},
});
describe('nx release preserve local dependency protocols', () => {
let previousPackageManager: string;
let e2eRegistryUrl: string;
beforeAll(() => {
previousPackageManager = process.env.SELECTED_PM;
// This is the verdaccio instance that the e2e tests themselves are working from
e2eRegistryUrl = execSync('npm config get registry').toString().trim();
});
afterEach(() => cleanupProject());
afterAll(() => {
process.env.SELECTED_PM = previousPackageManager;
});
/**
* Initialize each test with a fresh workspace using the specified
* package manager.
*/
const initializeProject = async (packageManager: 'pnpm' | 'bun') => {
process.env.SELECTED_PM = packageManager;
console.log(`Creating workspace with package manager: ${packageManager}`);
newProject({
packages: ['@nx/js'],
packageManager,
});
const pkg1 = uniq('my-pkg-1');
runCLI(`generate @nx/workspace:npm-package ${pkg1}`);
const pkg2 = uniq('my-pkg-2');
runCLI(`generate @nx/workspace:npm-package ${pkg2}`);
// Set up a workspace dependency using the workspace protocol
updateJson(join(pkg1, 'package.json'), (packageJson) => {
packageJson.dependencies = {
[`@proj/${pkg2}`]: 'workspace:*',
};
return packageJson;
});
// Add workspaces config
if (packageManager === 'pnpm') {
updateFile('pnpm-workspace.yaml', `packages:\n - ${pkg1}\n - ${pkg2}\n`);
} else {
updateJson('package.json', (packageJson) => {
packageJson.workspaces = [pkg1, pkg2];
return packageJson;
});
}
// workaround for NXC-143
runCLI('reset');
await runCommandAsync(getPackageManagerCommand({ packageManager }).install);
return { workspacePath: tmpProjPath(), pkg1, pkg2 };
};
it('should replace local dependency protocols with the actual version number when version.generatorOptions.preserveLocalDependencyProtocols is not set to true', async () => {
// The package manager currently does not matter for the versioning behavior, it's imperatively controlled by the user
const { workspacePath } = await initializeProject('pnpm');
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
nxJson.release = {};
return nxJson;
});
// Show the dependency being updated
expect(runCLI(`release version minor -d --verbose`, { cwd: workspacePath }))
.toMatchInlineSnapshot(`
NX Running release version for project: {project-name}
{project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json
{project-name} 📄 Resolved the current version as 0.0.0 from {project-name}/package.json
{project-name} 📄 Using the provided version specifier "minor".
{project-name} New version 0.1.0 written to {project-name}/package.json
NX Running release version for project: {project-name}
{project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json
{project-name} 📄 Resolved the current version as 0.0.0 from {project-name}/package.json
{project-name} 📄 Using the provided version specifier "minor".
{project-name} New version 0.1.0 written to {project-name}/package.json
{project-name} Applying new version 0.1.0 to 1 package which depends on {project-name}
"name": "@proj/{project-name}",
- "version": "0.0.0",
+ "version": "0.1.0",
"scripts": {
"dependencies": {
- "@proj/{project-name}": "workspace:*"
+ "@proj/{project-name}": "0.1.0"
}
}
+
"name": "@proj/{project-name}",
- "version": "0.0.0",
+ "version": "0.1.0",
"scripts": {
NX Updating PM lock file
Would update pnpm-lock.yaml with the following command, but --dry-run was set:
pnpm install --lockfile-only
NX Staging changed files with git
Would stage files in git with the following command, but --dry-run was set:
git add {project-name}/package.json {project-name}/package.json
`);
});
it('should preserve local dependency protocols when version.generatorOptions.preserveLocalDependencyProtocols is set to true', async () => {
// The package manager currently does not matter for the versioning behavior, it's imperatively controlled by the user
const { workspacePath } = await initializeProject('pnpm');
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
nxJson.release = {
version: {
generatorOptions: {
preserveLocalDependencyProtocols: true,
},
},
};
return nxJson;
});
// Show that the dependency has not been updated
expect(runCLI(`release version minor -d --verbose`, { cwd: workspacePath }))
.toMatchInlineSnapshot(`
NX Running release version for project: {project-name}
{project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json
{project-name} 📄 Resolved the current version as 0.0.0 from {project-name}/package.json
{project-name} 📄 Using the provided version specifier "minor".
{project-name} New version 0.1.0 written to {project-name}/package.json
NX Running release version for project: {project-name}
{project-name} 🔍 Reading data for package "@proj/{project-name}" from {project-name}/package.json
{project-name} 📄 Resolved the current version as 0.0.0 from {project-name}/package.json
{project-name} 📄 Using the provided version specifier "minor".
{project-name} New version 0.1.0 written to {project-name}/package.json
{project-name} Applying new version 0.1.0 to 1 package which depends on {project-name}
"name": "@proj/{project-name}",
- "version": "0.0.0",
+ "version": "0.1.0",
"scripts": {
}
+
"name": "@proj/{project-name}",
- "version": "0.0.0",
+ "version": "0.1.0",
"scripts": {
NX Updating PM lock file
Would update pnpm-lock.yaml with the following command, but --dry-run was set:
pnpm install --lockfile-only
NX Staging changed files with git
Would stage files in git with the following command, but --dry-run was set:
git add {project-name}/package.json {project-name}/package.json
`);
});
describe('pnpm publish', () => {
it('should replace local dependency protocols dynamically during publishing', async () => {
const { workspacePath, pkg1 } = await initializeProject('pnpm');
// Prove that the local dependency protocol is present in the pkg1 package.json
expect(readJson(join(workspacePath, pkg1, 'package.json')))
.toMatchInlineSnapshot(`
{
dependencies: {
@proj/{project-name}: workspace:*,
},
name: @proj/{project-name},
scripts: {
test: node index.js,
},
version: 0.0.0,
}
`);
// Publish the packages
expect(
runCLI(`release publish`, { silenceError: true, cwd: workspacePath })
).toMatchInlineSnapshot(`
NX Running target nx-release-publish for 2 projects:
- {project-name}
- {project-name}
> nx run {project-name}:nx-release-publish
📦 @proj/{project-name}@0.0.0
=== Tarball Contents ===
XXXXB index.js
XXXB package.json
XXB project.json
=== Tarball Details ===
name: @proj/{project-name}
version: 0.0.0
filename: proj-{project-name}-0.0.0.tgz
package size: XXXB
unpacked size: XXXB
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: X
Published to ${e2eRegistryUrl} with tag "latest"
> nx run {project-name}:nx-release-publish
📦 @proj/{project-name}@0.0.0
=== Tarball Contents ===
XXXXB index.js
XXXB package.json
XXB project.json
=== Tarball Details ===
name: @proj/{project-name}
version: 0.0.0
filename: proj-{project-name}-0.0.0.tgz
package size: XXXB
unpacked size: XXXB
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: X
Published to ${e2eRegistryUrl} with tag "latest"
NX Successfully ran target nx-release-publish for 2 projects
`);
// Ensure that the dependency on pkg2 specified on the registry was replaced with the actual version number during publishing
expect(
(await runCommandAsync(`npm view @proj/${pkg1} dependencies`))
.combinedOutput
).toMatchInlineSnapshot(`{ '@proj/{project-name}': '0.0.0' }`);
});
});
describe('bun publish', () => {
it('should replace local dependency protocols dynamically during publishing', async () => {
const { workspacePath, pkg1 } = await initializeProject('bun');
// Prove that the local dependency protocol is present in the pkg1 package.json
expect(readJson(join(workspacePath, pkg1, 'package.json')))
.toMatchInlineSnapshot(`
{
dependencies: {
@proj/{project-name}: workspace:*,
},
name: @proj/{project-name},
scripts: {
test: node index.js,
},
version: 0.0.0,
}
`);
// Publish the packages
expect(
runCLI(`release publish`, { silenceError: true, cwd: workspacePath })
).toMatchInlineSnapshot(`
NX Running target nx-release-publish for 2 projects:
- {project-name}
- {project-name}
> nx run {project-name}:nx-release-publish
bun publish vX.X.X ({COMMIT_SHA})
packed XXXB package.json
packed XXB index.js
packed XXB project.json
Total files: 3
Shasum: {SHASUM}
Integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unpacked size: XXXB
Packed size: XXXB
Tag: latest
Access: default
Registry: ${e2eRegistryUrl}
+ @proj/{project-name}@0.0.0
Published to ${e2eRegistryUrl} with tag "latest"
> nx run {project-name}:nx-release-publish
bun publish vX.X.X ({COMMIT_SHA})
packed XXXB package.json
packed XXB index.js
packed XXB project.json
Total files: 3
Shasum: {SHASUM}
Integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Unpacked size: XXXB
Packed size: XXXB
Tag: latest
Access: default
Registry: ${e2eRegistryUrl}
+ @proj/{project-name}@0.0.0
Published to ${e2eRegistryUrl} with tag "latest"
NX Successfully ran target nx-release-publish for 2 projects
`);
// Ensure that the dependency on pkg2 specified on the registry was replaced with the actual version number during publishing
expect(
(await runCommandAsync(`npm view @proj/${pkg1} dependencies`))
.combinedOutput
).toMatchInlineSnapshot(`{ '@proj/{project-name}': '0.0.0' }`);
});
});
});

View File

@ -170,7 +170,8 @@ export function getPackageManagerCommand({
runNx: `pnpm exec nx`, runNx: `pnpm exec nx`,
runNxSilent: `pnpm exec nx`, runNxSilent: `pnpm exec nx`,
runUninstalledPackage: 'pnpm dlx', runUninstalledPackage: 'pnpm dlx',
install: 'pnpm i', // We need to install with --no-frozen-lockfile when running e2e tests because pnpm will pick up the fact we are in CI and default to --frozen-lockfile
install: 'pnpm install --no-frozen-lockfile',
ciInstall: 'pnpm install --frozen-lockfile', ciInstall: 'pnpm install --frozen-lockfile',
addProd: isPnpmWorkspace ? 'pnpm add -w' : 'pnpm add', addProd: isPnpmWorkspace ? 'pnpm add -w' : 'pnpm add',
addDev: isPnpmWorkspace ? 'pnpm add -Dw' : 'pnpm add -D', addDev: isPnpmWorkspace ? 'pnpm add -Dw' : 'pnpm add -D',
@ -179,7 +180,8 @@ export function getPackageManagerCommand({
exec: pnpmVersion && gte(pnpmVersion, '6.13.0') ? 'pnpm exec' : 'pnpx', exec: pnpmVersion && gte(pnpmVersion, '6.13.0') ? 'pnpm exec' : 'pnpx',
}, },
bun: { bun: {
createWorkspace: `bunx create-nx-workspace@${publishedVersion}`, // See note in runCreateWorkspace in create-project-utils.ts for why we don't set @{version} for `bunx create-nx-workspace` right now
createWorkspace: `bunx create-nx-workspace`,
run: (script: string, args: string) => `bun run ${script} -- ${args}`, run: (script: string, args: string) => `bun run ${script} -- ${args}`,
runNx: `bunx nx`, runNx: `bunx nx`,
runNxSilent: `bunx nx`, runNxSilent: `bunx nx`,

View File

@ -1,4 +1,5 @@
import { copySync, ensureDirSync, moveSync, removeSync } from 'fs-extra'; import { copySync, ensureDirSync, moveSync, removeSync } from 'fs-extra';
import * as isCI from 'is-ci';
import { import {
createFile, createFile,
directoryExists, directoryExists,
@ -15,24 +16,22 @@ import {
isVerbose, isVerbose,
isVerboseE2ERun, isVerboseE2ERun,
} from './get-env-info'; } from './get-env-info';
import * as isCI from 'is-ci';
import { output, readJsonFile } from '@nx/devkit';
import { angularCliVersion as defaultAngularCliVersion } from '@nx/workspace/src/utils/versions'; import { angularCliVersion as defaultAngularCliVersion } from '@nx/workspace/src/utils/versions';
import { dump } from '@zkochan/js-yaml'; import { dump } from '@zkochan/js-yaml';
import { execSync, ExecSyncOptions } from 'child_process'; import { execSync, ExecSyncOptions } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { performance, PerformanceMeasure } from 'perf_hooks'; import { join } from 'node:path';
import { logError, logInfo } from './log-utils'; import { performance, PerformanceMeasure } from 'node:perf_hooks';
import { resetWorkspaceContext } from 'nx/src/utils/workspace-context';
import { import {
getPackageManagerCommand, getPackageManagerCommand,
runCLI, runCLI,
RunCmdOpts, RunCmdOpts,
runCommand, runCommand,
} from './command-utils'; } from './command-utils';
import { output, readJsonFile } from '@nx/devkit'; import { logError, logInfo } from './log-utils';
import { readFileSync } from 'fs';
import { join } from 'path';
import { resetWorkspaceContext } from 'nx/src/utils/workspace-context';
let projName: string; let projName: string;
@ -90,7 +89,10 @@ export function newProject({
let createNxWorkspaceMeasure: PerformanceMeasure; let createNxWorkspaceMeasure: PerformanceMeasure;
let packageInstallMeasure: PerformanceMeasure; let packageInstallMeasure: PerformanceMeasure;
if (!directoryExists(tmpBackupProjPath())) { // Namespace by package manager to avoid conflicts in test suites which include multiple package managers
const backupPath = tmpBackupProjPath(packageManager);
if (!directoryExists(backupPath)) {
const createNxWorkspaceStart = performance.mark( const createNxWorkspaceStart = performance.mark(
'create-nx-workspace:start' 'create-nx-workspace:start'
); );
@ -132,12 +134,12 @@ export function newProject({
stdio: isVerbose() ? 'inherit' : 'pipe', stdio: isVerbose() ? 'inherit' : 'pipe',
}); });
moveSync(`${e2eCwd}/proj`, `${tmpBackupProjPath()}`); moveSync(`${e2eCwd}/proj`, backupPath);
} }
projName = name; projName = name;
const projectDirectory = tmpProjPath(); const projectDirectory = tmpProjPath();
copySync(`${tmpBackupProjPath()}`, `${projectDirectory}`); copySync(backupPath, projectDirectory);
const dependencies = readJsonFile( const dependencies = readJsonFile(
`${projectDirectory}/package.json` `${projectDirectory}/package.json`
@ -257,6 +259,9 @@ export function runCreateWorkspace(
const pm = getPackageManagerCommand({ packageManager }); const pm = getPackageManagerCommand({ packageManager });
// Needed for bun workarounds, see below
const registry = execSync('npm config get registry').toString().trim();
let command = `${pm.createWorkspace} ${name} --preset=${preset} --nxCloud=skip --no-interactive`; let command = `${pm.createWorkspace} ${name} --preset=${preset} --nxCloud=skip --no-interactive`;
if (appName) { if (appName) {
@ -330,6 +335,51 @@ export function runCreateWorkspace(
command += ` --prefix=${prefix}`; command += ` --prefix=${prefix}`;
} }
if (packageManager === 'bun') {
/**
* `bunx` does not seem to work well at all with custom registries, I tried many combinations of flags and config files.
*
* The only viable workaround currently seems to be to write a package.json and a bunfig.toml in the e2e directory,
* install create-nx-workspace using `bun install` (which _does_ seem to respect the registry settings), and _then_
* run `bunx create-nx-workspace` (but without the version number added with @{version}).
*/
writeFileSync(
join(cwd, 'bunfig.toml'),
// Also set up a dedicated cache directory to hopefully avoid conflicts with the global cache
`
[install]
cache = ".bun-cache"
registry = "${registry}"
`.trim()
);
writeFileSync(
join(cwd, 'package.json'),
`
{
"private": true,
"name": "only-here-to-make-bunx-happy"
}
`
);
const output = execSync('bun install create-nx-workspace', {
cwd,
stdio: 'pipe',
env: {
CI: 'true',
...process.env,
},
encoding: 'utf-8',
});
const publishedVersion = getPublishedVersion();
// Ensure that it installed the version published for the e2e tests
if (!output.includes(publishedVersion)) {
console.error(output);
throw new Error(
`bunx create-nx-workspace did not install the version published for the e2e tests: ${publishedVersion}, in ${cwd}`
);
}
}
try { try {
const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, { const create = execSync(`${command}${isVerbose() ? ' --verbose' : ''}`, {
cwd, cwd,
@ -350,10 +400,32 @@ export function runCreateWorkspace(
}); });
} }
if (packageManager === 'bun') {
// We also have to add an explicit bunfig.toml in the workspace itself as bun does not seem to use the setting applied by the local registry logic
// (via `npm set config registry`), unlike all other package managers.
updateFile(
'bunfig.toml',
`
[install]
registry = { url = "${registry}", token = "secretVerdaccioToken" }
`.trim()
);
}
return create; return create;
} catch (e) { } catch (e) {
logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`); logError(`Original command: ${command}`, `${e.stdout}\n\n${e.stderr}`);
throw e; throw e;
} finally {
// Clean up files related to bun workarounds
if (packageManager === 'bun') {
removeSync(join(cwd, 'bunfig.toml'));
removeSync(join(cwd, 'package.json'));
removeSync(join(cwd, '.bun-cache'));
removeSync(join(cwd, 'node_modules'));
removeSync(join(cwd, 'bun.lock'));
removeSync(join(cwd, 'bun.lockb'));
}
} }
} }

View File

@ -1,7 +1,8 @@
import { readJsonFile, workspaceRoot } from '@nx/devkit'; import { readJsonFile, workspaceRoot } from '@nx/devkit';
import { execSync } from 'child_process';
import { existsSync } from 'fs-extra'; import { existsSync } from 'fs-extra';
import { execSync } from 'node:child_process';
import { join } from 'path'; import { join } from 'path';
import { gte } from 'semver';
import { dirSync } from 'tmp'; import { dirSync } from 'tmp';
import * as isCI from 'is-ci'; import * as isCI from 'is-ci';
@ -23,7 +24,7 @@ export function getPublishedVersion(): string {
} }
export function detectPackageManager(dir: string = ''): PackageManager { export function detectPackageManager(dir: string = ''): PackageManager {
return existsSync(join(dir, 'bun.lockb')) return existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))
? 'bun' ? 'bun'
: existsSync(join(dir, 'yarn.lock')) : existsSync(join(dir, 'yarn.lock'))
? 'yarn' ? 'yarn'
@ -121,7 +122,16 @@ 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', bun: (() => {
try {
// In version 1.2.0, bun switched to a text based lockfile format by default
return gte(execSync('bun --version').toString().trim(), '1.2.0')
? 'bun.lock'
: 'bun.lockb';
} catch {
return 'bun.lockb';
}
})(),
}; };
export function ensureCypressInstallation() { export function ensureCypressInstallation() {

View File

@ -1,6 +1,6 @@
import { execSync } from 'child_process'; import { execSync } from 'node:child_process';
import { existsSync, writeFileSync } from 'fs'; import { existsSync, writeFileSync } from 'node:fs';
import { join } from 'path'; import { join, sep } from 'node:path';
/* /*
* Because we don't want to depend on @nx/workspace (to speed up the workspace creation) * Because we don't want to depend on @nx/workspace (to speed up the workspace creation)
@ -12,7 +12,7 @@ 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, 'bun.lockb')) return existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))
? 'bun' ? 'bun'
: existsSync(join(dir, 'yarn.lock')) : existsSync(join(dir, 'yarn.lock'))
? 'yarn' ? 'yarn'
@ -83,7 +83,7 @@ export function getPackageManagerCommand(
getRegistryUrl: 'npm config get registry', getRegistryUrl: 'npm config get registry',
}; };
case 'bun': case 'bun':
// bun doesn't current support programatically reading config https://github.com/oven-sh/bun/issues/7140 // bun doesn't current support programmatically reading config https://github.com/oven-sh/bun/issues/7140
return { return {
install: 'bun install --silent --ignore-scripts', install: 'bun install --silent --ignore-scripts',
exec: 'bunx', exec: 'bunx',
@ -135,24 +135,25 @@ export function getPackageManagerVersion(
* - npx returns 'npm' * - npx returns 'npm'
* - pnpx returns 'pnpm' * - pnpx returns 'pnpm'
* - yarn create returns 'yarn' * - yarn create returns 'yarn'
* - bunx returns 'bun'
* *
* Default to 'npm' * Default to 'npm'
*/ */
export function detectInvokedPackageManager(): PackageManager { export function detectInvokedPackageManager(): PackageManager {
let detectedPackageManager: PackageManager = 'npm'; if (process.env.npm_config_user_agent) {
// mainModule is deprecated since Node 14, fallback for older versions for (const pm of packageManagerList) {
const invoker = require.main || process['mainModule']; if (process.env.npm_config_user_agent.startsWith(`${pm}/`)) {
return pm;
// default to `npm`
if (!invoker) {
return detectedPackageManager;
} }
for (const pkgManager of packageManagerList) {
if (invoker.path.includes(pkgManager)) {
detectedPackageManager = pkgManager;
break;
} }
} }
return detectedPackageManager; if (process.env.npm_execpath) {
for (const pm of packageManagerList) {
if (process.env.npm_execpath.split(sep).includes(pm)) {
return pm;
}
}
}
return 'npm';
} }

View File

@ -162,6 +162,7 @@ function replaceMentions(
'package-lock.json', 'package-lock.json',
'pnpm-lock.yaml', 'pnpm-lock.yaml',
'bun.lockb', 'bun.lockb',
'bun.lock',
'CHANGELOG.md', 'CHANGELOG.md',
]; ];
if (ignoredFiles.includes(basename(path))) { if (ignoredFiles.includes(basename(path))) {

View File

@ -2,29 +2,21 @@ import {
detectPackageManager, detectPackageManager,
generateFiles, generateFiles,
offsetFromRoot, offsetFromRoot,
PackageManager,
toJS, toJS,
Tree, Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import {
createNxCloudOnboardingURLForWelcomeApp,
getNxCloudAppOnBoardingUrl,
} from 'nx/src/nx-cloud/utilities/onboarding';
import { join } from 'path'; import { join } from 'path';
import { NormalizedSchema } from './normalize-options'; import { NormalizedSchema } from './normalize-options';
import {
getNxCloudAppOnBoardingUrl,
createNxCloudOnboardingURLForWelcomeApp,
} from 'nx/src/nx-cloud/utilities/onboarding';
export async function createApplicationFiles( export async function createApplicationFiles(
host: Tree, host: Tree,
options: NormalizedSchema options: NormalizedSchema
) { ) {
const packageManagerLockFile: Record<PackageManager, string> = {
npm: 'package-lock.json',
yarn: 'yarn.lock',
pnpm: 'pnpm-lock.yaml',
bun: 'bun.lockb',
};
const packageManager = detectPackageManager(host.root); const packageManager = detectPackageManager(host.root);
const packageLockFile = packageManagerLockFile[packageManager];
const onBoardingStatus = await createNxCloudOnboardingURLForWelcomeApp( const onBoardingStatus = await createNxCloudOnboardingURLForWelcomeApp(
host, host,
@ -43,7 +35,6 @@ export async function createApplicationFiles(
...options, ...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot), offsetFromRoot: offsetFromRoot(options.appProjectRoot),
packageManager, packageManager,
packageLockFile,
} }
); );
@ -56,7 +47,6 @@ export async function createApplicationFiles(
connectCloudUrl, connectCloudUrl,
offsetFromRoot: offsetFromRoot(options.appProjectRoot), offsetFromRoot: offsetFromRoot(options.appProjectRoot),
packageManager, packageManager,
packageLockFile,
} }
); );

View File

@ -51,21 +51,40 @@ export default async function runExecutor(
const packageName = packageJson.name; const packageName = packageJson.name;
/** /**
* pnpm supports dynamically updating locally linked packages during its packing phase, but other package managers do not. * Whether or not dynamically replacing local dependency protocols (such as "workspace:*") is supported during `nx release publish` is
* Therefore, protect the user from publishing invalid packages by checking if it contains local dependency protocols. * dependent on the package manager the user is using.
*
* npm does not support the workspace protocol at all, and `npm publish` does not support dynamically updating locally linked packages
* during its packing phase, so we give the user a clear error message informing them of that.
*
* - `pnpm publish` provides ideal support, it has the possibility of providing JSON output consistent with npm
* - `bun publish`, provides very good support, including all the flags we need apart from the JSON output, so we just have to accept that
* it will look and feel different and print what it gives us and perform one bit of string manipulation for the dry-run case.
* - `yarn npm publish`, IS NOT YET SUPPORTED, and will be tricky because it does not support the majority of the flags we need. However, it
* does support replacing local dependency protocols with the correct version during its packing phase.
*/ */
if (pm !== 'pnpm') { if (pm === 'npm' || pm === 'yarn') {
const depTypes = ['dependencies', 'devDependencies', 'peerDependencies']; const depTypes = ['dependencies', 'devDependencies', 'peerDependencies'];
for (const depType of depTypes) { for (const depType of depTypes) {
const deps = packageJson[depType]; const deps = packageJson[depType];
if (deps) { if (deps) {
for (const depName in deps) { for (const depName in deps) {
if (isLocallyLinkedPackageVersion(deps[depName])) { if (isLocallyLinkedPackageVersion(deps[depName])) {
if (pm === 'npm') {
console.error( console.error(
`Error: Cannot publish package "${packageName}" because it contains a local dependency protocol in its "${depType}", and your package manager is ${pm}. `Error: Cannot publish package "${packageName}" because it contains a local dependency protocol in its "${depType}", and your package manager is npm.
Please update the local dependency on "${depName}" to be a valid semantic version (e.g. using \`nx release\`) before publishing, or switch to pnpm as a package manager, which supports dynamically replacing these protocols during publishing.` Please update the local dependency on "${depName}" to be a valid semantic version (e.g. using \`nx release\`) before publishing, or switch to pnpm or bun as a package manager, which support dynamically replacing these protocols during publishing.`
); );
} else if (pm === 'yarn') {
console.error(
`Error: Cannot publish package "${packageName}" because it contains a local dependency protocol in its "${depType}", and your package manager is yarn.
Currently, yarn is not supported for this use case because its \`yarn npm publish\` command does not support the customization needed.
Please update the local dependency on "${depName}" to be a valid semantic version (e.g. using \`nx release\`) before publishing, or switch to pnpm or bun as a package manager, which support dynamically replacing these protocols during publishing.`
);
}
return { return {
success: false, success: false,
}; };
@ -245,7 +264,10 @@ Please update the local dependency on "${depName}" to be a valid semantic versio
* JSON output under the name of the package in that case (and it would need to be handled below). * JSON output under the name of the package in that case (and it would need to be handled below).
*/ */
const publishCommandSegments = [ const publishCommandSegments = [
pm === 'pnpm' pm === 'bun'
? // Unlike npm, bun publish does not support a custom registryConfigKey option
`bun publish --cwd="${packageRoot}" --json --registry="${registry}" --tag=${tag}`
: pm === 'pnpm'
? // Unlike npm, pnpm publish does not support a custom registryConfigKey option, and will error on uncommitted changes by default if --no-git-checks is not set ? // Unlike npm, pnpm publish does not support a custom registryConfigKey option, and will error on uncommitted changes by default if --no-git-checks is not set
`pnpm publish "${packageRoot}" --json --registry="${registry}" --tag=${tag} --no-git-checks` `pnpm publish "${packageRoot}" --json --registry="${registry}" --tag=${tag} --no-git-checks`
: `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`, : `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`,
@ -271,6 +293,30 @@ Please update the local dependency on "${depName}" to be a valid semantic versio
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: false, windowsHide: false,
}); });
// If in dry-run mode, the version on disk will not represent the version that would be published, so we scrub it from the output to avoid confusion.
const dryRunVersionPlaceholder = 'X.X.X-dry-run';
const publishSummaryMessage = isDryRun
? `Would publish to ${registry} with tag "${tag}", but ${chalk.keyword(
'orange'
)('[dry-run]')} was set`
: `Published to ${registry} with tag "${tag}"`;
// bun publish does not support outputting JSON, so we need to modify and print the output string directly
if (pm === 'bun') {
let outputStr = output.toString();
if (isDryRun) {
outputStr = outputStr.replace(
new RegExp(`${packageJson.name}@${packageJson.version}`, 'g'),
`${packageJson.name}@${dryRunVersionPlaceholder}`
);
}
console.log(outputStr);
console.log(publishSummaryMessage);
return {
success: true,
};
}
/** /**
* We cannot JSON.parse the output directly because if the user is using lifecycle scripts, npm/pnpm will mix its publish output with the JSON output all on stdout. * We cannot JSON.parse the output directly because if the user is using lifecycle scripts, npm/pnpm will mix its publish output with the JSON output all on stdout.
@ -287,8 +333,6 @@ Please update the local dependency on "${depName}" to be a valid semantic versio
}; };
} }
// If in dry-run mode, the version on disk will not represent the version that would be published, so we scrub it from the output to avoid confusion.
const dryRunVersionPlaceholder = 'X.X.X-dry-run';
if (isDryRun) { if (isDryRun) {
for (const [key, val] of Object.entries(jsonData)) { for (const [key, val] of Object.entries(jsonData)) {
if (typeof val !== 'string') { if (typeof val !== 'string') {
@ -314,21 +358,24 @@ Please update the local dependency on "${depName}" to be a valid semantic versio
console.log(afterJsonData); console.log(afterJsonData);
} }
if (isDryRun) { // Print the summary message after the JSON data has been printed
console.log( console.log(publishSummaryMessage);
`Would publish to ${registry} with tag "${tag}", but ${chalk.keyword(
'orange'
)('[dry-run]')} was set`
);
} else {
console.log(`Published to ${registry} with tag "${tag}"`);
}
return { return {
success: true, success: true,
}; };
} catch (err) { } catch (err) {
try { try {
// bun publish does not support outputting JSON, so we cannot perform any further processing
if (pm === 'bun') {
console.error(`bun publish error:`);
console.error(err.stderr?.toString() || '');
console.error(err.stdout?.toString() || '');
return {
success: false,
};
}
const stdoutData = JSON.parse(err.stdout?.toString() || '{}'); const stdoutData = JSON.parse(err.stdout?.toString() || '{}');
console.error(`${pm} publish error:`); console.error(`${pm} publish error:`);

View File

@ -419,6 +419,7 @@ function lockFileHashChanged(): boolean {
join(workspaceRoot, 'yarn.lock'), join(workspaceRoot, 'yarn.lock'),
join(workspaceRoot, 'pnpm-lock.yaml'), join(workspaceRoot, 'pnpm-lock.yaml'),
join(workspaceRoot, 'bun.lockb'), join(workspaceRoot, 'bun.lockb'),
join(workspaceRoot, 'bun.lock'),
] ]
.filter((file) => existsSync(file)) .filter((file) => existsSync(file))
.map((file) => hashFile(file)); .map((file) => hashFile(file));

View File

@ -3,8 +3,10 @@
* It encapsulates the package manager specific logic and implementation details. * It encapsulates the package manager specific logic and implementation details.
*/ */
import { readFileSync, existsSync } from 'fs'; import { execSync } from 'node:child_process';
import { join } from 'path'; import { readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { gte } from 'semver';
import { import {
detectPackageManager, detectPackageManager,
@ -46,17 +48,20 @@ 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';
const BUN_LOCK_FILE = 'bun.lockb'; const BUN_LOCK_FILE = 'bun.lockb';
const BUN_TEXT_LOCK_FILE = 'bun.lock';
export const LOCKFILES = [ export const LOCKFILES = [
YARN_LOCK_FILE, YARN_LOCK_FILE,
NPM_LOCK_FILE, NPM_LOCK_FILE,
PNPM_LOCK_FILE, PNPM_LOCK_FILE,
BUN_LOCK_FILE, BUN_LOCK_FILE,
BUN_TEXT_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); const BUN_LOCK_PATH = join(workspaceRoot, BUN_LOCK_FILE);
const BUN_TEXT_LOCK_PATH = join(workspaceRoot, BUN_TEXT_LOCK_FILE);
/** /**
* Parses lock file and maps dependencies and metadata to {@link LockFileGraph} * Parses lock file and maps dependencies and metadata to {@link LockFileGraph}
@ -143,7 +148,7 @@ export function lockFileExists(packageManager: PackageManager): boolean {
return existsSync(NPM_LOCK_PATH); return existsSync(NPM_LOCK_PATH);
} }
if (packageManager === 'bun') { if (packageManager === 'bun') {
return existsSync(BUN_LOCK_PATH); return existsSync(BUN_LOCK_PATH) || existsSync(BUN_TEXT_LOCK_PATH);
} }
throw new Error( throw new Error(
`Unknown package manager ${packageManager} or lock file missing` `Unknown package manager ${packageManager} or lock file missing`
@ -182,7 +187,16 @@ function getLockFilePath(packageManager: PackageManager): string {
return NPM_LOCK_PATH; return NPM_LOCK_PATH;
} }
if (packageManager === 'bun') { if (packageManager === 'bun') {
try {
const bunVersion = execSync('bun --version').toString().trim();
// In version 1.2.0, bun switched to a text based lockfile format by default
if (gte(bunVersion, '1.2.0')) {
return BUN_TEXT_LOCK_FILE;
}
return BUN_LOCK_PATH; return BUN_LOCK_PATH;
} catch {
return BUN_LOCK_PATH;
}
} }
throw new Error(`Unknown package manager: ${packageManager}`); throw new Error(`Unknown package manager: ${packageManager}`);
} }

View File

@ -42,6 +42,7 @@ describe('getTouchedProjectsFromLockFile', () => {
'pnpm-lock.yaml', 'pnpm-lock.yaml',
'pnpm-lock.yml', 'pnpm-lock.yml',
'bun.lockb', 'bun.lockb',
'bun.lock',
].forEach((lockFile) => { ].forEach((lockFile) => {
describe(`"${lockFile}"`, () => { describe(`"${lockFile}"`, () => {
it(`should not return changes when "${lockFile}" is not touched`, () => { it(`should not return changes when "${lockFile}" is not touched`, () => {

View File

@ -26,6 +26,7 @@ export const getTouchedProjectsFromLockFile: TouchedProjectLocator<
'pnpm-lock.yaml', 'pnpm-lock.yaml',
'pnpm-lock.yml', 'pnpm-lock.yml',
'bun.lockb', 'bun.lockb',
'bun.lock',
]; ];
if (fileChanges.some((f) => lockFiles.includes(f.file))) { if (fileChanges.some((f) => lockFiles.includes(f.file))) {

View File

@ -29,8 +29,14 @@ describe('package-manager', () => {
packageManager: 'pnpm', packageManager: 'pnpm',
}, },
}); });
const packageManager = detectPackageManager(); expect(detectPackageManager()).toEqual('pnpm');
expect(packageManager).toEqual('pnpm');
jest.spyOn(configModule, 'readNxJson').mockReturnValueOnce({
cli: {
packageManager: 'yarn',
},
});
expect(detectPackageManager()).toEqual('yarn');
}); });
it('should detect yarn package manager from yarn.lock', () => { it('should detect yarn package manager from yarn.lock', () => {
@ -45,13 +51,15 @@ describe('package-manager', () => {
return false; return false;
case 'bun.lockb': case 'bun.lockb':
return false; return false;
case 'bun.lock':
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(2, 'yarn.lock'); expect(fs.existsSync).toHaveBeenNthCalledWith(3, 'yarn.lock');
}); });
it('should detect pnpm package manager from pnpm-lock.yaml', () => { it('should detect pnpm package manager from pnpm-lock.yaml', () => {
@ -66,13 +74,15 @@ describe('package-manager', () => {
return false; return false;
case 'bun.lockb': case 'bun.lockb':
return false; return false;
case 'bun.lock':
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('pnpm'); expect(packageManager).toEqual('pnpm');
expect(fs.existsSync).toHaveBeenCalledTimes(3); expect(fs.existsSync).toHaveBeenCalledTimes(4);
}); });
it('should detect bun package manager from bun.lockb', () => { it('should detect bun package manager from bun.lockb', () => {
@ -87,6 +97,8 @@ describe('package-manager', () => {
return false; return false;
case 'bun.lockb': case 'bun.lockb':
return true; return true;
case 'bun.lock':
return false;
default: default:
return jest.requireActual('fs').existsSync(p); return jest.requireActual('fs').existsSync(p);
} }
@ -96,6 +108,29 @@ describe('package-manager', () => {
expect(fs.existsSync).toHaveBeenCalledTimes(1); expect(fs.existsSync).toHaveBeenCalledTimes(1);
}); });
it('should detect bun package manager from bun.lock', () => {
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.lock':
return true;
case 'bun.lockb':
return false;
default:
return jest.requireActual('fs').existsSync(p);
}
});
const packageManager = detectPackageManager();
expect(packageManager).toEqual('bun');
expect(fs.existsSync).toHaveBeenCalledTimes(2);
});
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) => {
@ -108,13 +143,15 @@ describe('package-manager', () => {
return false; return false;
case 'bun.lockb': case 'bun.lockb':
return false; return false;
case 'bun.lock':
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(3); expect(fs.existsSync).toHaveBeenCalledTimes(4);
}); });
}); });

View File

@ -53,7 +53,7 @@ export function detectPackageManager(dir: string = ''): PackageManager {
const nxJson = readNxJson(); const nxJson = readNxJson();
return ( return (
nxJson.cli?.packageManager ?? nxJson.cli?.packageManager ??
(existsSync(join(dir, 'bun.lockb')) (existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock'))
? 'bun' ? 'bun'
: existsSync(join(dir, 'yarn.lock')) : existsSync(join(dir, 'yarn.lock'))
? 'yarn' ? 'yarn'
@ -185,7 +185,7 @@ export function getPackageManagerCommand(
}; };
}, },
bun: () => { bun: () => {
// bun doesn't current support programatically reading config https://github.com/oven-sh/bun/issues/7140 // bun doesn't current support programmatically reading config https://github.com/oven-sh/bun/issues/7140
return { return {
install: 'bun install', install: 'bun install',
ciInstall: 'bun install --no-cache', ciInstall: 'bun install --no-cache',

View File

@ -38,7 +38,8 @@ jest.mock('fs', () => {
existsSync: (p) => existsSync: (p) =>
p.endsWith('yarn.lock') || p.endsWith('yarn.lock') ||
p.endsWith('pnpm-lock.yaml') || p.endsWith('pnpm-lock.yaml') ||
p.endsWith('bun.lockb') p.endsWith('bun.lockb') ||
p.endsWith('bun.lock')
? memFs.existsSync(p) ? memFs.existsSync(p)
: actualFs.existsSync(p), : actualFs.existsSync(p),
}; };
@ -87,7 +88,7 @@ describe('CI Workflow generator', () => {
beforeEach(() => { beforeEach(() => {
let fileSys; let fileSys;
if (packageManager === 'bun') { if (packageManager === 'bun') {
fileSys = { 'bun.lockb': '' }; fileSys = { 'bun.lock': '' };
} else if (packageManager === 'yarn') { } else if (packageManager === 'yarn') {
fileSys = { 'yarn.lock': '' }; fileSys = { 'yarn.lock': '' };
} else if (packageManager === 'pnpm') { } else if (packageManager === 'pnpm') {

View File

@ -12,6 +12,11 @@ function checkLockFiles() {
'Invalid occurence of "bun.lockb" file. Please remove it and use only "pnpm-lock.yaml"' 'Invalid occurence of "bun.lockb" file. Please remove it and use only "pnpm-lock.yaml"'
); );
} }
if (fs.existsSync('bun.lock')) {
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"'