From 578405862df65a5728699672c6dfc3c0533957d9 Mon Sep 17 00:00:00 2001 From: Jonathan Gelin Date: Wed, 18 Jun 2025 11:16:59 +0200 Subject: [PATCH] feat(js): support publishing with registryConfigKey when pnpm >=9.15.7 <10.0.0 || >=10.5.0 (#31622) --- .../release-publish/release-publish.impl.ts | 10 +-- packages/nx/src/utils/package-manager.spec.ts | 63 +++++++++++++++++++ packages/nx/src/utils/package-manager.ts | 30 ++++++++- 3 files changed, 94 insertions(+), 9 deletions(-) diff --git a/packages/js/src/executors/release-publish/release-publish.impl.ts b/packages/js/src/executors/release-publish/release-publish.impl.ts index b7fa121547..49b39db753 100644 --- a/packages/js/src/executors/release-publish/release-publish.impl.ts +++ b/packages/js/src/executors/release-publish/release-publish.impl.ts @@ -1,5 +1,6 @@ import { detectPackageManager, + getPackageManagerCommand, ExecutorContext, readJsonFile, } from '@nx/devkit'; @@ -263,14 +264,9 @@ Please update the local dependency on "${depName}" to be a valid semantic versio * to running from the package root directly), then special attention should be paid to the fact that npm/pnpm publish will nest its * JSON output under the name of the package in that case (and it would need to be handled below). */ + const pmCommand = getPackageManagerCommand(pm); const publishCommandSegments = [ - 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 - `pnpm publish "${packageRoot}" --json --registry="${registry}" --tag=${tag} --no-git-checks` - : `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`, + pmCommand.publish(packageRoot, registry, registryConfigKey, tag), ]; if (options.otp) { diff --git a/packages/nx/src/utils/package-manager.spec.ts b/packages/nx/src/utils/package-manager.spec.ts index b7d19a3c02..3a0f395516 100644 --- a/packages/nx/src/utils/package-manager.spec.ts +++ b/packages/nx/src/utils/package-manager.spec.ts @@ -10,6 +10,7 @@ import * as fileUtils from '../utils/fileutils'; import { addPackagePathToWorkspaces, detectPackageManager, + getPackageManagerCommand, getPackageManagerVersion, getPackageWorkspaces, isWorkspacesEnabled, @@ -547,4 +548,66 @@ describe('package-manager', () => { ).toEqual('3.2.3'); }); }); + + describe('getPackageManagerCommand', () => { + const publishCmdParam: [string, string, string, string] = [ + 'dist/packages/my-pkg', + 'https://registry.npmjs.org/', + '@org:registry', + 'latest', + ]; + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return npm publish command', () => { + const commands = getPackageManagerCommand('npm'); + expect(commands.publish(...publishCmdParam)).toEqual( + 'npm publish "dist/packages/my-pkg" --json --"@org:registry=https://registry.npmjs.org/" --tag=latest' + ); + }); + + it('should return yarn publish command using npm publish', () => { + const commands = getPackageManagerCommand('yarn'); + expect(commands.publish(...publishCmdParam)).toEqual( + 'npm publish "dist/packages/my-pkg" --json --"@org:registry=https://registry.npmjs.org/" --tag=latest' + ); + }); + + it('should return pnpm publish command with scoped registry when provided for pnpm version >= 9.15.7 < 10.0.0 || >= 10.5.0', () => { + jest.spyOn(childProcess, 'execSync').mockImplementation((p) => { + switch (p) { + case 'pnpm --version': + return '9.15.7'; + } + }); + const commands = getPackageManagerCommand('pnpm'); + expect(commands.publish(...publishCmdParam)).toEqual( + 'pnpm publish "dist/packages/my-pkg" --json --"@org:registry=https://registry.npmjs.org/" --tag=latest --no-git-checks' + ); + }); + + it('should return pnpm publish command without use scoped registry for pnpm version < 9.15.7', () => { + jest.spyOn(childProcess, 'execSync').mockImplementation((p) => { + switch (p) { + case 'pnpm --version': + return '9.10.1'; + default: + throw new Error('Command failed'); + } + }); + jest.spyOn(fileUtils, 'readJsonFile').mockReturnValueOnce({}); + const commands = getPackageManagerCommand('pnpm'); + expect(commands.publish(...publishCmdParam)).toEqual( + 'pnpm publish "dist/packages/my-pkg" --json --"registry=https://registry.npmjs.org/" --tag=latest --no-git-checks' + ); + }); + + it('should return bun publish command with registry and tag', () => { + const commands = getPackageManagerCommand('bun'); + expect(commands.publish(...publishCmdParam)).toEqual( + 'bun publish --cwd="dist/packages/my-pkg" --json --registry="https://registry.npmjs.org/" --tag=latest' + ); + }); + }); }); diff --git a/packages/nx/src/utils/package-manager.ts b/packages/nx/src/utils/package-manager.ts index 3ed9f5f676..c4d7296353 100644 --- a/packages/nx/src/utils/package-manager.ts +++ b/packages/nx/src/utils/package-manager.ts @@ -11,7 +11,7 @@ import { } from 'yaml'; import { rm } from 'node:fs/promises'; import { dirname, join, relative } from 'path'; -import { gte, lt, parse } from 'semver'; +import { gte, lt, parse, satisfies } from 'semver'; import { dirSync } from 'tmp'; import { promisify } from 'util'; @@ -44,6 +44,12 @@ export interface PackageManagerCommands { run: (script: string, args?: string) => string; // Make this required once bun adds programatically support for reading config https://github.com/oven-sh/bun/issues/7140 getRegistryUrl?: string; + publish: ( + packageRoot: string, + registry: string, + registryConfigKey: string, + tag: string + ) => string; } /** @@ -130,17 +136,28 @@ export function getPackageManagerCommand( getRegistryUrl: useBerry ? 'yarn config get npmRegistryServer' : 'yarn config get registry', + publish: (packageRoot, registry, registryConfigKey, tag) => + `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`, }; }, pnpm: () => { - let modernPnpm: boolean, includeDoubleDashBeforeArgs: boolean; + let modernPnpm: boolean, + includeDoubleDashBeforeArgs: boolean, + allowRegistryConfigKey: boolean; try { const pnpmVersion = getPackageManagerVersion('pnpm', root); modernPnpm = gte(pnpmVersion, '6.13.0'); includeDoubleDashBeforeArgs = lt(pnpmVersion, '7.0.0'); + // Support for --@scope:registry was added in pnpm v10.5.0 and backported to v9.15.7. + // Versions >=10.0.0 and <10.5.0 do NOT support this CLI option. + allowRegistryConfigKey = satisfies( + pnpmVersion, + '>=9.15.7 <10.0.0 || >=10.5.0' + ); } catch { modernPnpm = true; includeDoubleDashBeforeArgs = true; + allowRegistryConfigKey = false; } const isPnpmWorkspace = existsSync(join(root, 'pnpm-workspace.yaml')); @@ -163,6 +180,10 @@ export function getPackageManagerCommand( }`, list: 'pnpm ls --depth 100', getRegistryUrl: 'pnpm config get registry', + publish: (packageRoot, registry, registryConfigKey, tag) => + `pnpm publish "${packageRoot}" --json --"${ + allowRegistryConfigKey ? registryConfigKey : 'registry' + }=${registry}" --tag=${tag} --no-git-checks`, }; }, npm: () => { @@ -182,6 +203,8 @@ export function getPackageManagerCommand( `npm run ${script}${args ? ' -- ' + args : ''}`, list: 'npm ls', getRegistryUrl: 'npm config get registry', + publish: (packageRoot, registry, registryConfigKey, tag) => + `npm publish "${packageRoot}" --json --"${registryConfigKey}=${registry}" --tag=${tag}`, }; }, bun: () => { @@ -197,6 +220,9 @@ export function getPackageManagerCommand( dlx: 'bunx', run: (script: string, args: string) => `bun run ${script} -- ${args}`, list: 'bun pm ls', + // Unlike npm, bun publish does not support a custom registryConfigKey option + publish: (packageRoot, registry, registryConfigKey, tag) => + `bun publish --cwd="${packageRoot}" --json --registry="${registry}" --tag=${tag}`, }; }, };