From dc2841b5bb4c72a271c4d6c3f344b62a1e7b87ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Dieuze?= Date: Tue, 4 Aug 2026 10:36:46 +0200 Subject: [PATCH 1/2] [rush] Fix cross-subspace workspace:* deps failing under pnpm 11 (emit globalPnpmfile via pnpm-workspace.yaml) --- ...e-global-pnpmfile_2026-08-04-08-30-00.json | 10 +++++ .../src/logic/base/BaseInstallManager.ts | 3 ++ .../logic/installManager/InstallHelpers.ts | 10 +++++ .../src/logic/pnpm/PnpmWorkspaceFile.ts | 20 +++++++-- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 26 ++++++++++++ .../PnpmWorkspaceFile.test.ts.snap | 7 ++++ .../src/logic/test/InstallHelpers.test.ts | 41 +++++++++++++++++++ .../__snapshots__/InstallHelpers.test.ts.snap | 4 ++ .../common/config/rush/pnpm-config.json | 3 ++ .../common/config/rush/subspaces.json | 5 +++ .../config/subspaces/default/pnpm-config.json | 1 + .../test/pnpmConfigPnpm11Subspaces/rush.json | 5 +++ 12 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/pnpm-config.json create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/subspaces.json create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/subspaces/default/pnpm-config.json create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/rush.json diff --git a/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json b/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json new file mode 100644 index 00000000000..8fa74d75b80 --- /dev/null +++ b/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix cross-subspace \"workspace:*\" dependencies failing with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND under pnpm 11 when subspaces are enabled. The subspace \"global pnpmfile\" was wired up via a \"global-pnpmfile=\" line in the generated .npmrc, but pnpm 11 only reads auth/registry settings from .npmrc; for pnpm 11+ Rush now emits the path via the \"globalPnpmfile\" setting of the generated pnpm-workspace.yaml. Behavior for pnpm 10 and earlier is unchanged.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index a7016129ae6..c83e713f763 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -537,6 +537,9 @@ export abstract class BaseInstallManager { extraNpmrcLines.push(...commonNpmrcFileLines); } + // NOTE: pnpm 11+ only reads auth/registry settings from .npmrc, so it ignores this line; + // for pnpm 11+ the global pnpmfile path is emitted via the generated pnpm-workspace.yaml + // instead (see InstallHelpers.resolvePnpmSettings). The line is kept for pnpm 10 and earlier. extraNpmrcLines.push( `global-pnpmfile=${subspace.getSubspaceTempFolderPath()}/${RushConstants.pnpmfileGlobalFilename}` ); diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 397c039ed2e..220df476bdc 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -339,6 +339,16 @@ export class InstallHelpers { workspaceFile.trustPolicy = trustPolicy; workspaceFile.trustPolicyExclude = trustPolicyExclude; workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; + + if (rushConfiguration.subspacesFeatureEnabled) { + // When subspaces are enabled, Rush generates a "global pnpmfile" that rewrites + // cross-subspace "workspace:*" dependency specifiers to "link:" specifiers. For pnpm 10 and + // earlier it is wired up via a "global-pnpmfile=" line in the generated .npmrc (see + // BaseInstallManager), but pnpm 11+ only reads auth/registry settings from .npmrc, so that + // line is silently ignored and installation fails with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND. + // For pnpm 11+, emit the path via the generated pnpm-workspace.yaml instead. + workspaceFile.globalPnpmfile = `${subspace.getSubspaceTempFolderPath()}/${RushConstants.pnpmfileGlobalFilename}`; + } } else { // For older pnpm, these settings live in the "pnpm" field of package.json. packageJsonPnpmSection = { diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 8653b00c523..7734ee575e1 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -111,6 +111,15 @@ interface IPnpmWorkspaceYaml { * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) */ minimumReleaseAgeExclude: string[] | undefined; + /** + * The path to the "global pnpmfile" that Rush generates when subspaces are enabled, which rewrites + * cross-subspace `workspace:*` dependency specifiers to `link:` specifiers. pnpm 11+ only reads + * auth/registry settings from `.npmrc`, so the `global-pnpmfile=` line Rush writes there is + * silently ignored; for pnpm 11+ the path is emitted here instead. Without it, installation of a + * subspace with cross-subspace dependencies fails with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND. + * (USED ONLY IN PNPM 11.0.0 AND NEWER) + */ + globalPnpmfile: string | undefined; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -133,6 +142,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { public trustPolicyIgnoreAfter: IPnpmWorkspaceYaml['trustPolicyIgnoreAfter']; public minimumReleaseAge: IPnpmWorkspaceYaml['minimumReleaseAge']; public minimumReleaseAgeExclude: IPnpmWorkspaceYaml['minimumReleaseAgeExclude']; + public globalPnpmfile: IPnpmWorkspaceYaml['globalPnpmfile']; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -190,7 +200,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { trustPolicyExclude, trustPolicyIgnoreAfter, minimumReleaseAge, - minimumReleaseAgeExclude + minimumReleaseAgeExclude, + globalPnpmfile } = workspaceYaml; workspaceFile.catalogs = catalogs; workspaceFile.allowBuilds = allowBuilds; @@ -205,6 +216,7 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; workspaceFile.minimumReleaseAge = minimumReleaseAge; workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; + workspaceFile.globalPnpmfile = globalPnpmfile; } return workspaceFile; @@ -236,7 +248,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { trustPolicyExclude, trustPolicyIgnoreAfter, minimumReleaseAge, - minimumReleaseAgeExclude + minimumReleaseAgeExclude, + globalPnpmfile } = this; // Ensure stable sort order when serializing Sort.sortSet(workspacePackages); @@ -256,7 +269,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { trustPolicyExclude, trustPolicyIgnoreAfter, minimumReleaseAge, - minimumReleaseAgeExclude + minimumReleaseAgeExclude, + globalPnpmfile }; const yamlModule: typeof import('js-yaml') = await import('js-yaml'); diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts index 87f9da1d54e..d34b55191ea 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -384,6 +384,19 @@ describe(PnpmWorkspaceFile.name, () => { }); }); + it('reads globalPnpmfile from an existing workspace file', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + workspaceFile.globalPnpmfile = '/repo/common/temp/my-subspace/global-pnpmfile.cjs'; + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + const loadedWorkspaceFile: PnpmWorkspaceFile | undefined = + await PnpmWorkspaceFile.tryLoadAsync(workspaceFilePath); + expect(loadedWorkspaceFile?.globalPnpmfile).toEqual( + '/repo/common/temp/my-subspace/global-pnpmfile.cjs' + ); + }); + it('returns undefined when the workspace file has no patchedDependencies', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); workspaceFile.addPackage(`${projectsDir}/app1`); @@ -448,6 +461,19 @@ describe(PnpmWorkspaceFile.name, () => { }); }); + describe('globalPnpmfile functionality', () => { + it('generates workspace file with globalPnpmfile', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.globalPnpmfile = '/repo/common/temp/my-subspace/global-pnpmfile.cjs'; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toMatchSnapshot(); + }); + }); + describe('minimumReleaseAge functionality', () => { it('generates workspace file with minimumReleaseAge', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); diff --git a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap index d230c6623c0..b0eff80d3aa 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap +++ b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap @@ -126,6 +126,13 @@ trustPolicyIgnoreAfter: 1440 " `; +exports[`PnpmWorkspaceFile globalPnpmfile functionality generates workspace file with globalPnpmfile 1`] = ` +"globalPnpmfile: /repo/common/temp/my-subspace/global-pnpmfile.cjs +packages: + - projects/app1 +" +`; + exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge 1`] = ` "minimumReleaseAge: 20160 packages: diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 15a8d909696..cfc2319219a 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -117,6 +117,47 @@ describe(InstallHelpers.name, () => { expect(workspaceFile?.trustPolicy).toEqual('no-downgrade'); expect(workspaceFile?.trustPolicyExclude).toEqual(['chokidar@4.0.3']); expect(workspaceFile?.trustPolicyIgnoreAfter).toEqual(1440); + + // The subspaces feature is not enabled in this repo, so no global pnpmfile is emitted. + expect(workspaceFile?.globalPnpmfile).toBeUndefined(); + }); + + it('emits the subspace global pnpmfile path via pnpm-workspace.yaml for pnpm 11', async () => { + const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfigPnpm11Subspaces/rush.json`; + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + const pnpmSettings = InstallHelpers.resolvePnpmSettings( + rushConfiguration, + rushConfiguration.defaultSubspace, + terminal + ); + + // pnpm 11+ only reads auth/registry settings from .npmrc, so the "global-pnpmfile=" line in + // the generated .npmrc is ignored; the path must be emitted via pnpm-workspace.yaml instead, + // otherwise cross-subspace "workspace:*" dependencies fail with + // ERR_PNPM_WORKSPACE_PKG_NOT_FOUND. + const workspaceFile: PnpmWorkspaceFile | undefined = + TestUtilities.stripAnnotations(pnpmSettings)?.workspaceFile; + expect(workspaceFile?.globalPnpmfile).toEqual( + `${rushConfiguration.defaultSubspace.getSubspaceTempFolderPath()}/global-pnpmfile.cjs` + ); + }); + + it('does not emit the global pnpmfile via pnpm-workspace.yaml for pnpm < 11', async () => { + const RUSH_JSON_FILENAME: string = `${__dirname}/repoWithSubspaces/rush.json`; + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + const pnpmSettings = InstallHelpers.resolvePnpmSettings( + rushConfiguration, + rushConfiguration.defaultSubspace, + terminal + ); + + // For pnpm 10 and earlier the global pnpmfile stays wired up via the generated .npmrc + // (see BaseInstallManager); the workspace file must not carry it. + const workspaceFile: PnpmWorkspaceFile | undefined = + TestUtilities.stripAnnotations(pnpmSettings)?.workspaceFile; + expect(workspaceFile?.globalPnpmfile).toBeUndefined(); }); }); }); diff --git a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap index c9b8a524b39..002fcde9038 100644 --- a/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap +++ b/libraries/rush-lib/src/logic/test/__snapshots__/InstallHelpers.test.ts.snap @@ -1,7 +1,11 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`InstallHelpers generateCommonPackageJsonAsync does not emit the global pnpmfile via pnpm-workspace.yaml for pnpm < 11: Terminal Output 1`] = `Array []`; + exports[`InstallHelpers generateCommonPackageJsonAsync does not generate a "pnpm" field for pnpm 11 (all settings belong in pnpm-workspace.yaml): Terminal Output 1`] = `Array []`; +exports[`InstallHelpers generateCommonPackageJsonAsync emits the subspace global pnpmfile path via pnpm-workspace.yaml for pnpm 11: Terminal Output 1`] = `Array []`; + exports[`InstallHelpers generateCommonPackageJsonAsync generates correct package json with pnpm configurations 1`] = ` Object { "dependencies": Object {}, diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/pnpm-config.json new file mode 100644 index 00000000000..e783afd001e --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/pnpm-config.json @@ -0,0 +1,3 @@ +{ + "useWorkspaces": true +} diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/subspaces.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/subspaces.json new file mode 100644 index 00000000000..fefe0ee4878 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/rush/subspaces.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": true, + "subspaceNames": ["extra-subspace"] +} diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/subspaces/default/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/subspaces/default/pnpm-config.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/common/config/subspaces/default/pnpm-config.json @@ -0,0 +1 @@ +{} diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/rush.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/rush.json new file mode 100644 index 00000000000..de05d6e5169 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11Subspaces/rush.json @@ -0,0 +1,5 @@ +{ + "pnpmVersion": "11.0.0", + "rushVersion": "5.58.0", + "projects": [] +} From b3fe4638e1109003d38aaea59e519c1383318b98 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 5 Aug 2026 20:22:48 -0700 Subject: [PATCH 2/2] Apply suggestion from @bmiddha --- ...x-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json b/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json index 8fa74d75b80..8a316848947 100644 --- a/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json +++ b/common/changes/@microsoft/rush/fix-pnpm11-subspace-global-pnpmfile_2026-08-04-08-30-00.json @@ -2,8 +2,8 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "Fix cross-subspace \"workspace:*\" dependencies failing with ERR_PNPM_WORKSPACE_PKG_NOT_FOUND under pnpm 11 when subspaces are enabled. The subspace \"global pnpmfile\" was wired up via a \"global-pnpmfile=\" line in the generated .npmrc, but pnpm 11 only reads auth/registry settings from .npmrc; for pnpm 11+ Rush now emits the path via the \"globalPnpmfile\" setting of the generated pnpm-workspace.yaml. Behavior for pnpm 10 and earlier is unchanged.", - "type": "none" + "comment": "Fix cross-subspace `workspace:*` dependency failures with pnpm 11.", + "type": "patch" } ], "packageName": "@microsoft/rush"