From e6331edf8dc1b961282fc8fe565c3f53fe49bc8f Mon Sep 17 00:00:00 2001 From: Bruno Paulino Date: Thu, 18 Jun 2026 13:12:20 +0200 Subject: [PATCH 1/3] [rush] Write pnpm global settings to pnpm-workspace.yaml for pnpm 11 pnpm 11 no longer reads the "pnpm" field of package.json, so the settings Rush serialized there were silently ignored. Relocate globalOverrides, globalPackageExtensions, globalPeerDependencyRules, globalAllowedDeprecatedVersions, and globalPatchedDependencies to the generated common/temp/pnpm-workspace.yaml for pnpm >= 11.0.0, mirroring the allowBuilds relocation from #5817. Behavior for older pnpm is unchanged. Fixes #5837 --- .../logic/installManager/InstallHelpers.ts | 19 +- .../installManager/WorkspaceInstallManager.ts | 29 +- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 106 ++++++++ .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 255 ++++++++++++++++++ .../PnpmWorkspaceFile.test.ts.snap | 70 +++++ 5 files changed, 468 insertions(+), 11 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 1296125a98..a1783e90b9 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -65,19 +65,24 @@ export class InstallHelpers { commonPackageJson.pnpm = {}; } - if (pnpmOptions.globalOverrides) { + const pnpmVersion: string = rushConfiguration.packageManagerToolVersion; + + // pnpm 11 no longer reads the "pnpm" field of package.json. For pnpm 11+, these settings + // are written to common/temp/pnpm-workspace.yaml by WorkspaceInstallManager instead. + // See https://github.com/microsoft/rushstack/issues/5837 + const isPnpm11OrNewer: boolean = semver.gte(pnpmVersion, '11.0.0'); + + if (!isPnpm11OrNewer && pnpmOptions.globalOverrides) { commonPackageJson.pnpm.overrides = pnpmOptions.globalOverrides; } - if (pnpmOptions.globalPackageExtensions) { + if (!isPnpm11OrNewer && pnpmOptions.globalPackageExtensions) { commonPackageJson.pnpm.packageExtensions = pnpmOptions.globalPackageExtensions; } - if (pnpmOptions.globalPeerDependencyRules) { + if (!isPnpm11OrNewer && pnpmOptions.globalPeerDependencyRules) { commonPackageJson.pnpm.peerDependencyRules = pnpmOptions.globalPeerDependencyRules; } - const pnpmVersion: string = rushConfiguration.packageManagerToolVersion; - if (pnpmOptions.globalNeverBuiltDependencies) { if (semver.gte(pnpmVersion, '11.0.0')) { terminal.writeWarningLine( @@ -134,11 +139,11 @@ export class InstallHelpers { commonPackageJson.pnpm.ignoredOptionalDependencies = pnpmOptions.globalIgnoredOptionalDependencies; } - if (pnpmOptions.globalAllowedDeprecatedVersions) { + if (!isPnpm11OrNewer && pnpmOptions.globalAllowedDeprecatedVersions) { commonPackageJson.pnpm.allowedDeprecatedVersions = pnpmOptions.globalAllowedDeprecatedVersions; } - if (pnpmOptions.globalPatchedDependencies) { + if (!isPnpm11OrNewer && pnpmOptions.globalPatchedDependencies) { commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies; } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 222dd45141..6001d88392 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -476,10 +476,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ) { if (pnpmOptions.globalAllowBuilds) { workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds); - } else if ( - pnpmOptions.globalOnlyBuiltDependencies || - pnpmOptions.globalNeverBuiltDependencies - ) { + } else if (pnpmOptions.globalOnlyBuiltDependencies || pnpmOptions.globalNeverBuiltDependencies) { // Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies // to allowBuilds format for pnpm 11+ const allowBuilds: Record = {}; @@ -509,6 +506,30 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); } + // For pnpm 11+, the following settings must be written to pnpm-workspace.yaml because pnpm 11 + // no longer reads the "pnpm" field of package.json (where Rush writes them for older pnpm). + // See https://github.com/microsoft/rushstack/issues/5837 + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.gte(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '11.0.0') + ) { + if (pnpmOptions.globalOverrides) { + workspaceFile.setOverrides(pnpmOptions.globalOverrides); + } + if (pnpmOptions.globalPackageExtensions) { + workspaceFile.setPackageExtensions(pnpmOptions.globalPackageExtensions); + } + if (pnpmOptions.globalPeerDependencyRules) { + workspaceFile.setPeerDependencyRules(pnpmOptions.globalPeerDependencyRules); + } + if (pnpmOptions.globalAllowedDeprecatedVersions) { + workspaceFile.setAllowedDeprecatedVersions(pnpmOptions.globalAllowedDeprecatedVersions); + } + if (pnpmOptions.globalPatchedDependencies) { + workspaceFile.setPatchedDependencies(pnpmOptions.globalPatchedDependencies); + } + } + // Save the generated workspace file. Don't update the file timestamp unless the content has changed, // since "rush install" will consider this timestamp workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true }); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 850aaf1687..b1f924a0fa 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -9,6 +9,7 @@ import { Sort, Import, Path } from '@rushstack/node-core-library'; import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile'; import { PNPM_SHRINKWRAP_YAML_FORMAT } from './PnpmYamlCommon'; +import type { IPnpmPackageExtension, IPnpmPeerDependencyRules } from './PnpmOptionsConfiguration'; const yamlModule: typeof import('js-yaml') = Import.lazy('js-yaml', require); @@ -42,6 +43,36 @@ interface IPnpmWorkspaceYaml { * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) */ allowBuilds?: Record; + /** + * Dependency version overrides. In pnpm 11+ this replaces the `pnpm.overrides` field of + * `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + overrides?: Record; + /** + * Extensions applied to the `package.json` of matched dependencies. In pnpm 11+ this replaces + * the `pnpm.packageExtensions` field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + packageExtensions?: Record; + /** + * Rules for suppressing peer dependency validation errors. In pnpm 11+ this replaces the + * `pnpm.peerDependencyRules` field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + peerDependencyRules?: IPnpmPeerDependencyRules; + /** + * Suppresses installation warnings for deprecated package versions. In pnpm 11+ this replaces + * the `pnpm.allowedDeprecatedVersions` field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + allowedDeprecatedVersions?: Record; + /** + * Patches applied to dependencies. In pnpm 11+ this replaces the `pnpm.patchedDependencies` + * field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) + */ + patchedDependencies?: Record; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -53,6 +84,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { private _workspacePackages: Set; private _catalogs: Record> | undefined; private _allowBuilds: Record | undefined; + private _overrides: Record | undefined; + private _packageExtensions: Record | undefined; + private _peerDependencyRules: IPnpmPeerDependencyRules | undefined; + private _allowedDeprecatedVersions: Record | undefined; + private _patchedDependencies: Record | undefined; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -67,6 +103,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._workspacePackages = new Set(); this._catalogs = undefined; this._allowBuilds = undefined; + this._overrides = undefined; + this._packageExtensions = undefined; + this._peerDependencyRules = undefined; + this._allowedDeprecatedVersions = undefined; + this._patchedDependencies = undefined; } /** @@ -86,6 +127,51 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._allowBuilds = allowBuilds; } + /** + * Sets the dependency version overrides for the workspace. + * In pnpm 11+ this replaces the `pnpm.overrides` field of `package.json`. + * @param overrides - A map of package selector to version + */ + public setOverrides(overrides: Record | undefined): void { + this._overrides = overrides; + } + + /** + * Sets the package extensions for the workspace. + * In pnpm 11+ this replaces the `pnpm.packageExtensions` field of `package.json`. + * @param packageExtensions - A map of package selector to package.json extension + */ + public setPackageExtensions(packageExtensions: Record | undefined): void { + this._packageExtensions = packageExtensions; + } + + /** + * Sets the peer dependency rules for the workspace. + * In pnpm 11+ this replaces the `pnpm.peerDependencyRules` field of `package.json`. + * @param peerDependencyRules - The peer dependency rules + */ + public setPeerDependencyRules(peerDependencyRules: IPnpmPeerDependencyRules | undefined): void { + this._peerDependencyRules = peerDependencyRules; + } + + /** + * Sets the allowed deprecated versions for the workspace. + * In pnpm 11+ this replaces the `pnpm.allowedDeprecatedVersions` field of `package.json`. + * @param allowedDeprecatedVersions - A map of package name to version range + */ + public setAllowedDeprecatedVersions(allowedDeprecatedVersions: Record | undefined): void { + this._allowedDeprecatedVersions = allowedDeprecatedVersions; + } + + /** + * Sets the patched dependencies for the workspace. + * In pnpm 11+ this replaces the `pnpm.patchedDependencies` field of `package.json`. + * @param patchedDependencies - A map of package name and version to patch file path + */ + public setPatchedDependencies(patchedDependencies: Record | undefined): void { + this._patchedDependencies = patchedDependencies; + } + /** @override */ public addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file @@ -115,6 +201,26 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceYaml.allowBuilds = this._allowBuilds; } + if (this._overrides && Object.keys(this._overrides).length > 0) { + workspaceYaml.overrides = this._overrides; + } + + if (this._packageExtensions && Object.keys(this._packageExtensions).length > 0) { + workspaceYaml.packageExtensions = this._packageExtensions; + } + + if (this._peerDependencyRules && Object.keys(this._peerDependencyRules).length > 0) { + workspaceYaml.peerDependencyRules = this._peerDependencyRules; + } + + if (this._allowedDeprecatedVersions && Object.keys(this._allowedDeprecatedVersions).length > 0) { + workspaceYaml.allowedDeprecatedVersions = this._allowedDeprecatedVersions; + } + + if (this._patchedDependencies && Object.keys(this._patchedDependencies).length > 0) { + workspaceYaml.patchedDependencies = this._patchedDependencies; + } + return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT); } } 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 a113bcc0b8..efb23abb8b 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -242,4 +242,259 @@ describe(PnpmWorkspaceFile.name, () => { expect(content).not.toContain('allowBuilds'); }); }); + + describe('overrides functionality', () => { + it('generates workspace file with overrides', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setOverrides({ + 'foo@1.0.0': '1.0.1', + bar: '^2.0.0' + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('overrides:'); + expect(content).toMatchSnapshot(); + }); + + it('handles empty overrides object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setOverrides({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('overrides'); + }); + + it('handles undefined overrides', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setOverrides(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('overrides'); + }); + }); + + describe('packageExtensions functionality', () => { + it('generates workspace file with packageExtensions', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPackageExtensions({ + 'react@*': { + dependencies: { + foo: '1.0.0' + } + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('packageExtensions:'); + expect(content).toMatchSnapshot(); + }); + + it('handles empty packageExtensions object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPackageExtensions({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('packageExtensions'); + }); + + it('handles undefined packageExtensions', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPackageExtensions(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('packageExtensions'); + }); + }); + + describe('peerDependencyRules functionality', () => { + it('generates workspace file with peerDependencyRules', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPeerDependencyRules({ + ignoreMissing: ['baz'], + allowedVersions: { + react: '18' + } + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('peerDependencyRules:'); + expect(content).toMatchSnapshot(); + }); + + it('handles empty peerDependencyRules object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPeerDependencyRules({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('peerDependencyRules'); + }); + + it('handles undefined peerDependencyRules', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPeerDependencyRules(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('peerDependencyRules'); + }); + }); + + describe('allowedDeprecatedVersions functionality', () => { + it('generates workspace file with allowedDeprecatedVersions', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowedDeprecatedVersions({ + querystring: '*' + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('allowedDeprecatedVersions:'); + expect(content).toMatchSnapshot(); + }); + + it('handles empty allowedDeprecatedVersions object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowedDeprecatedVersions({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('allowedDeprecatedVersions'); + }); + + it('handles undefined allowedDeprecatedVersions', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setAllowedDeprecatedVersions(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('allowedDeprecatedVersions'); + }); + }); + + describe('patchedDependencies functionality', () => { + it('generates workspace file with patchedDependencies', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPatchedDependencies({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('patchedDependencies:'); + expect(content).toMatchSnapshot(); + }); + + it('handles empty patchedDependencies object', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPatchedDependencies({}); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('patchedDependencies'); + }); + + it('handles undefined patchedDependencies', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setPatchedDependencies(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('patchedDependencies'); + }); + }); + + describe('combined pnpm 11 settings', () => { + it('generates workspace file with all relocated settings together', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setCatalogs({ + default: { + react: '^18.0.0' + } + }); + workspaceFile.setAllowBuilds({ + esbuild: true + }); + workspaceFile.setOverrides({ + 'foo@1.0.0': '1.0.1' + }); + workspaceFile.setPackageExtensions({ + 'react@*': { + dependencies: { + foo: '1.0.0' + } + } + }); + workspaceFile.setPeerDependencyRules({ + allowedVersions: { + react: '18' + } + }); + workspaceFile.setAllowedDeprecatedVersions({ + querystring: '*' + }); + workspaceFile.setPatchedDependencies({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + }); }); 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 18f15e7456..c79f2b9a84 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 @@ -21,6 +21,14 @@ packages: " `; +exports[`PnpmWorkspaceFile allowedDeprecatedVersions functionality generates workspace file with allowedDeprecatedVersions 1`] = ` +"allowedDeprecatedVersions: + querystring: '*' +packages: + - projects/app1 +" +`; + exports[`PnpmWorkspaceFile basic functionality generates workspace file with packages only 1`] = ` "packages: - projects/app1 @@ -86,3 +94,65 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] = - projects/app1 " `; + +exports[`PnpmWorkspaceFile combined pnpm 11 settings generates workspace file with all relocated settings together 1`] = ` +"allowBuilds: + esbuild: true +allowedDeprecatedVersions: + querystring: '*' +catalogs: + default: + react: ^18.0.0 +overrides: + foo@1.0.0: 1.0.1 +packageExtensions: + react@*: + dependencies: + foo: 1.0.0 +packages: + - projects/app1 +patchedDependencies: + lodash@4.17.21: patches/lodash@4.17.21.patch +peerDependencyRules: + allowedVersions: + react: '18' +" +`; + +exports[`PnpmWorkspaceFile overrides functionality generates workspace file with overrides 1`] = ` +"overrides: + bar: ^2.0.0 + foo@1.0.0: 1.0.1 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile packageExtensions functionality generates workspace file with packageExtensions 1`] = ` +"packageExtensions: + react@*: + dependencies: + foo: 1.0.0 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile patchedDependencies functionality generates workspace file with patchedDependencies 1`] = ` +"packages: + - projects/app1 +patchedDependencies: + lodash@4.17.21: patches/lodash@4.17.21.patch +" +`; + +exports[`PnpmWorkspaceFile peerDependencyRules functionality generates workspace file with peerDependencyRules 1`] = ` +"packages: + - projects/app1 +peerDependencyRules: + allowedVersions: + react: '18' + ignoreMissing: + - baz +" +`; From ff78c07e0e05c1d17d94b7da50fcc2f305216a57 Mon Sep 17 00:00:00 2001 From: Bruno Paulino Date: Thu, 18 Jun 2026 13:16:35 +0200 Subject: [PATCH 2/3] [rush] Add change file for pnpm 11 settings relocation Co-references #5837 --- ...-relocate-global-settings_2026-06-18-11-15-37.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json diff --git a/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json b/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json new file mode 100644 index 0000000000..b56b1b1d50 --- /dev/null +++ b/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix pnpm 11 silently ignoring the `globalOverrides`, `globalPackageExtensions`, `globalPeerDependencyRules`, `globalAllowedDeprecatedVersions`, and `globalPatchedDependencies` settings from pnpm-config.json. Because pnpm 11 no longer reads the `pnpm` field of package.json, Rush now writes these settings to the generated `common/temp/pnpm-workspace.yaml` for pnpm 11+ (matching the existing `allowBuilds` relocation). Behavior for pnpm 10 and earlier is unchanged.", + "type": "patch", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "brunojppb@users.noreply.github.com" +} From c906b41aef4eadf5e5339d5b4d3505fa161ef396 Mon Sep 17 00:00:00 2001 From: Bruno Paulino Date: Thu, 18 Jun 2026 16:13:40 +0200 Subject: [PATCH 3/3] [rush] Fix rush-pnpm patch-commit for pnpm 11 and add coverage Two fixes from adversarial review of the pnpm 11 settings relocation: 1. rush-pnpm patch-commit / patch-remove read patchedDependencies from common/temp/package.json, which pnpm 11 no longer populates. They now read patchedDependencies from common/temp/pnpm-workspace.yaml for pnpm >= 11 (mirroring the approve-builds allowBuilds path), preserving the package.json path for pnpm < 11 and the existing subspace + patches-folder logic. 2. Added integration coverage: - InstallHelpers.generateCommonPackageJson omits the relocated settings from package.json for pnpm 11 and still writes them for pnpm < 11. - RushPnpmCommandLineParser reads patchedDependencies from pnpm-workspace.yaml for pnpm 11 and from package.json for pnpm < 11. Refs #5837 --- ...e-global-settings_2026-06-18-11-15-37.json | 2 +- .../src/cli/RushPnpmCommandLineParser.ts | 26 ++++- .../test/RushPnpmCommandLineParser.test.ts | 110 ++++++++++++++++++ .../src/logic/test/InstallHelpers.test.ts | 35 ++++++ .../__snapshots__/InstallHelpers.test.ts.snap | 2 + .../common/config/rush/pnpm-config.json | 12 ++ .../common/config/rush/pnpm-config.json | 25 ++++ .../src/logic/test/pnpmConfigPnpm11/rush.json | 5 + 8 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/common/config/rush/pnpm-config.json create mode 100644 libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/rush.json diff --git a/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json b/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json index b56b1b1d50..083c8421d9 100644 --- a/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json +++ b/common/changes/@microsoft/rush/pnpm11-relocate-global-settings_2026-06-18-11-15-37.json @@ -1,7 +1,7 @@ { "changes": [ { - "comment": "Fix pnpm 11 silently ignoring the `globalOverrides`, `globalPackageExtensions`, `globalPeerDependencyRules`, `globalAllowedDeprecatedVersions`, and `globalPatchedDependencies` settings from pnpm-config.json. Because pnpm 11 no longer reads the `pnpm` field of package.json, Rush now writes these settings to the generated `common/temp/pnpm-workspace.yaml` for pnpm 11+ (matching the existing `allowBuilds` relocation). Behavior for pnpm 10 and earlier is unchanged.", + "comment": "Fix pnpm 11 silently ignoring the `globalOverrides`, `globalPackageExtensions`, `globalPeerDependencyRules`, `globalAllowedDeprecatedVersions`, and `globalPatchedDependencies` settings from pnpm-config.json. Because pnpm 11 no longer reads the `pnpm` field of package.json, Rush now writes these settings to the generated `common/temp/pnpm-workspace.yaml` for pnpm 11+ (matching the existing `allowBuilds` relocation), and `rush-pnpm patch-commit`/`patch-remove` now read `patchedDependencies` back from `pnpm-workspace.yaml` for pnpm 11+. Behavior for pnpm 10 and earlier is unchanged.", "type": "patch", "packageName": "@microsoft/rush" } diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index c71c3735c0..e6a8a015ed 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -537,12 +537,28 @@ export class RushPnpmCommandLineParser { break; } - // Example: "C:\MyRepo\common\temp\package.json" - const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`; - const commonPackageJson: JsonObject = JsonFile.load(commonPackageJsonFilename); - const newGlobalPatchedDependencies: Record | undefined = - commonPackageJson?.pnpm?.patchedDependencies; const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); + const pnpmVersion: string = this._rushConfiguration.packageManagerToolVersion; + const semver: typeof import('semver') = await import('semver'); + + let newGlobalPatchedDependencies: Record | undefined; + if (semver.gte(pnpmVersion, '11.0.0')) { + // PNPM 11+ stores patchedDependencies in pnpm-workspace.yaml instead of the package.json "pnpm" field + const workspaceYamlFilename: string = `${subspaceTempFolder}/pnpm-workspace.yaml`; + const yamlModule: typeof import('js-yaml') = await import('js-yaml'); + const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename); + const workspaceYaml: { patchedDependencies?: Record } = (yamlModule.load( + workspaceYamlContent + ) ?? {}) as { patchedDependencies?: Record }; + newGlobalPatchedDependencies = workspaceYaml?.patchedDependencies; + } else { + // PNPM 10.x and earlier store patchedDependencies in the package.json "pnpm" field + // Example: "C:\MyRepo\common\temp\package.json" + const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`; + const commonPackageJson: JsonObject = JsonFile.load(commonPackageJsonFilename); + newGlobalPatchedDependencies = commonPackageJson?.pnpm?.patchedDependencies; + } + const currentGlobalPatchedDependencies: Record | undefined = pnpmOptions?.globalPatchedDependencies; diff --git a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts index 966866c838..b37b08f684 100644 --- a/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts +++ b/libraries/rush-lib/src/cli/test/RushPnpmCommandLineParser.test.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { FileSystem, JsonFile } from '@rushstack/node-core-library'; + import { RushPnpmCommandLineParser } from '../RushPnpmCommandLineParser'; interface IRushPnpmCommandLineParserInternals { @@ -13,6 +15,48 @@ async function validatePnpmArgsAsync(pnpmArgs: string[]): Promise { return pnpmArgs; } +interface IPnpmOptionsStub { + globalPatchedDependencies: Record | undefined; + updateGlobalPatchedDependencies: jest.Mock; +} + +interface ISubspaceStub { + getSubspaceTempFolderPath(): string; + getSubspaceConfigFolderPath(): string; + getSubspacePnpmPatchesFolderPath(): string; + getPnpmOptions(): IPnpmOptionsStub | undefined; +} + +interface IPostExecuteInternals { + _commandName?: string; + _subspace: ISubspaceStub; + _rushConfiguration: { packageManagerToolVersion: string }; + _terminal: { writeWarningLine: jest.Mock; writeErrorLine: jest.Mock }; + _doRushUpdateAsync: jest.Mock; + _postExecuteAsync(): Promise; +} + +const SUBSPACE_TEMP_FOLDER: string = '/repo/common/temp'; + +function createPostExecuteParser( + commandName: string, + pnpmVersion: string, + pnpmOptions: IPnpmOptionsStub +): IPostExecuteInternals { + const parser: IPostExecuteInternals = Object.create(RushPnpmCommandLineParser.prototype); + parser._commandName = commandName; + parser._rushConfiguration = { packageManagerToolVersion: pnpmVersion }; + parser._terminal = { writeWarningLine: jest.fn(), writeErrorLine: jest.fn() }; + parser._doRushUpdateAsync = jest.fn().mockResolvedValue(undefined); + parser._subspace = { + getSubspaceTempFolderPath: () => SUBSPACE_TEMP_FOLDER, + getSubspaceConfigFolderPath: () => '/repo/common/config/rush', + getSubspacePnpmPatchesFolderPath: () => '/repo/common/config/rush/pnpm-patches', + getPnpmOptions: () => pnpmOptions + }; + return parser; +} + describe(RushPnpmCommandLineParser.name, () => { it('adds recursive mode to workspace query commands by default', async () => { await expect(validatePnpmArgsAsync(['outdated'])).resolves.toEqual(['outdated', '--recursive']); @@ -34,3 +78,69 @@ describe(RushPnpmCommandLineParser.name, () => { await expect(validatePnpmArgsAsync(['outdated', '--global'])).resolves.toEqual(['outdated', '--global']); }); }); + +describe('RushPnpmCommandLineParser patch-commit patchedDependencies sync', () => { + let readFileAsyncSpy: jest.SpyInstance; + let existsSpy: jest.SpyInstance; + let jsonLoadSpy: jest.SpyInstance; + + afterEach(() => { + readFileAsyncSpy?.mockRestore(); + existsSpy?.mockRestore(); + jsonLoadSpy?.mockRestore(); + }); + + it('reads patchedDependencies from pnpm-workspace.yaml for pnpm >= 11', async () => { + const pnpmOptions: IPnpmOptionsStub = { + globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, + updateGlobalPatchedDependencies: jest.fn() + }; + const parser: IPostExecuteInternals = createPostExecuteParser('patch-commit', '11.7.0', pnpmOptions); + + const workspaceYaml: string = + 'packages:\n' + + ' - ../../app\n' + + 'patchedDependencies:\n' + + ' lodash@4.17.21: patches/lodash@4.17.21.patch\n'; + readFileAsyncSpy = jest.spyOn(FileSystem, 'readFileAsync').mockResolvedValue(workspaceYaml); + existsSpy = jest.spyOn(FileSystem, 'exists').mockReturnValue(false); + // If the code incorrectly read package.json for pnpm 11, it would pick up this sentinel value. + jsonLoadSpy = jest + .spyOn(JsonFile, 'load') + .mockReturnValue({ pnpm: { patchedDependencies: { 'should-not-be-used@1.0.0': 'x.patch' } } }); + + await parser._postExecuteAsync(); + + expect(readFileAsyncSpy).toHaveBeenCalledWith(`${SUBSPACE_TEMP_FOLDER}/pnpm-workspace.yaml`); + expect(jsonLoadSpy).not.toHaveBeenCalled(); + expect(pnpmOptions.updateGlobalPatchedDependencies).toHaveBeenCalledWith({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + expect(parser._doRushUpdateAsync).toHaveBeenCalledTimes(1); + }); + + it('reads patchedDependencies from package.json for pnpm < 11', async () => { + const pnpmOptions: IPnpmOptionsStub = { + globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, + updateGlobalPatchedDependencies: jest.fn() + }; + const parser: IPostExecuteInternals = createPostExecuteParser('patch-commit', '10.27.0', pnpmOptions); + + existsSpy = jest.spyOn(FileSystem, 'exists').mockReturnValue(false); + readFileAsyncSpy = jest.spyOn(FileSystem, 'readFileAsync').mockResolvedValue(''); + jsonLoadSpy = jest + .spyOn(JsonFile, 'load') + .mockReturnValue({ + pnpm: { patchedDependencies: { 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' } } + }); + + await parser._postExecuteAsync(); + + expect(jsonLoadSpy).toHaveBeenCalledWith(`${SUBSPACE_TEMP_FOLDER}/package.json`); + expect(readFileAsyncSpy).not.toHaveBeenCalled(); + expect(pnpmOptions.updateGlobalPatchedDependencies).toHaveBeenCalledWith({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + expect(parser._doRushUpdateAsync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index c6441d7484..86127b9e4f 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -58,6 +58,7 @@ describe('InstallHelpers', () => { 'bar@^2.1.0': '3.0.0', 'qar@1>zoo': '2' }, + // For pnpm < 11 all of these settings are still written into the package.json "pnpm" field. packageExtensions: { 'react-redux': { peerDependencies: { @@ -65,6 +66,18 @@ describe('InstallHelpers', () => { } } }, + peerDependencyRules: { + allowedVersions: { + react: '18' + }, + ignoreMissing: ['@babel/core'] + }, + allowedDeprecatedVersions: { + request: '*' + }, + patchedDependencies: { + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }, neverBuiltDependencies: ['fsevents', 'level'], onlyBuiltDependencies: ['esbuild', 'playwright'], pnpmFutureFeature: true @@ -72,5 +85,27 @@ describe('InstallHelpers', () => { }) ); }); + + it('omits the relocated pnpm settings for pnpm 11 (they belong in pnpm-workspace.yaml)', () => { + const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfigPnpm11/rush.json`; + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + InstallHelpers.generateCommonPackageJson( + rushConfiguration, + rushConfiguration.defaultSubspace, + undefined, + terminal + ); + const packageJson: IPackageJson = mockJsonFileSave.mock.calls[0][0]; + const pnpmField: Record = ( + TestUtilities.stripAnnotations(packageJson) as unknown as { pnpm: Record } + ).pnpm; + // For pnpm >= 11 these are written to common/temp/pnpm-workspace.yaml instead of package.json. + expect(pnpmField).not.toHaveProperty('overrides'); + expect(pnpmField).not.toHaveProperty('packageExtensions'); + expect(pnpmField).not.toHaveProperty('peerDependencyRules'); + expect(pnpmField).not.toHaveProperty('allowedDeprecatedVersions'); + expect(pnpmField).not.toHaveProperty('patchedDependencies'); + }); }); }); 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 080d68c7ba..74e06a92a8 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,3 +1,5 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`InstallHelpers generateCommonPackageJson generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; + +exports[`InstallHelpers generateCommonPackageJson omits the relocated pnpm settings for pnpm 11 (they belong in pnpm-workspace.yaml): Terminal Output 1`] = `Array []`; diff --git a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json index 6151e8b1ba..7b1bf48614 100644 --- a/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/src/logic/test/pnpmConfig/common/config/rush/pnpm-config.json @@ -12,6 +12,18 @@ } } }, + "globalPeerDependencyRules": { + "allowedVersions": { + "react": "18" + }, + "ignoreMissing": ["@babel/core"] + }, + "globalAllowedDeprecatedVersions": { + "request": "*" + }, + "globalPatchedDependencies": { + "lodash@4.17.21": "patches/lodash@4.17.21.patch" + }, "globalNeverBuiltDependencies": ["fsevents", "level"], "globalOnlyBuiltDependencies": ["esbuild", "playwright"], "globalCatalogs": { diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/common/config/rush/pnpm-config.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/common/config/rush/pnpm-config.json new file mode 100644 index 0000000000..0c863d5ac5 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/common/config/rush/pnpm-config.json @@ -0,0 +1,25 @@ +{ + "globalOverrides": { + "foo": "^1.0.0", + "bar@^2.1.0": "3.0.0" + }, + "globalPackageExtensions": { + "react-redux": { + "peerDependencies": { + "react-dom": "*" + } + } + }, + "globalPeerDependencyRules": { + "allowedVersions": { + "react": "18" + }, + "ignoreMissing": ["@babel/core"] + }, + "globalAllowedDeprecatedVersions": { + "request": "*" + }, + "globalPatchedDependencies": { + "lodash@4.17.21": "patches/lodash@4.17.21.patch" + } +} diff --git a/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/rush.json b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/rush.json new file mode 100644 index 0000000000..de05d6e516 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/pnpmConfigPnpm11/rush.json @@ -0,0 +1,5 @@ +{ + "pnpmVersion": "11.0.0", + "rushVersion": "5.58.0", + "projects": [] +}