From 357418294165a48b31769aefcc169d4591a8ba54 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 17:13:30 -0400 Subject: [PATCH 1/7] Make PnpmWorkspaceFile.tryLoadAsync return undefined when the workspace file is missing Rename loadAsync to tryLoadAsync so it returns undefined (instead of throwing) when pnpm-workspace.yaml does not exist, and update the rush-pnpm patch-commit and approve-builds callers accordingly. --- .../src/cli/RushPnpmCommandLineParser.ts | 8 +-- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 43 +++++++++--- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 70 +++++++++++-------- 3 files changed, 77 insertions(+), 44 deletions(-) diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 39a0c81a02..89bcf467a2 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -545,10 +545,10 @@ export class RushPnpmCommandLineParser { 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 workspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync( + const workspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync( `${subspaceTempFolder}/${RushConstants.pnpmWorkspaceFileName}` ); - newGlobalPatchedDependencies = workspaceFile.patchedDependencies; + newGlobalPatchedDependencies = workspaceFile?.patchedDependencies; } else { // PNPM 10.x and earlier store patchedDependencies in the package.json "pnpm" field // Example: "C:\MyRepo\common\temp\package.json" @@ -613,10 +613,10 @@ export class RushPnpmCommandLineParser { if (semver.gte(pnpmVersion, '11.0.0')) { // PNPM 11+ uses allowBuilds in pnpm-workspace.yaml instead of onlyBuiltDependencies in package.json - const workspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync( + const workspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync( `${subspaceTempFolder}/${RushConstants.pnpmWorkspaceFileName}` ); - const newGlobalAllowBuilds: Record | undefined = workspaceFile.allowBuilds; + const newGlobalAllowBuilds: Record | undefined = workspaceFile?.allowBuilds; const currentGlobalAllowBuilds: Record | undefined = pnpmOptions?.globalAllowBuilds; diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 0495b3388d..56a22de16b 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -124,8 +124,18 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { * * @param workspaceYamlFilename - The path to the `pnpm-workspace.yaml` file */ - public static async loadAsync(workspaceYamlFilename: string): Promise { - const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename); + public static async tryLoadAsync(workspaceYamlFilename: string): Promise { + let workspaceYamlContent: string; + try { + workspaceYamlContent = await FileSystem.readFileAsync(workspaceYamlFilename); + } catch (error) { + if (FileSystem.isNotExistError(error)) { + return undefined; + } else { + throw error; + } + } + const yamlModule: typeof import('js-yaml') = await import('js-yaml'); const workspaceYaml: IPnpmWorkspaceYaml | undefined = yamlModule.load(workspaceYamlContent) as | IPnpmWorkspaceYaml @@ -133,15 +143,26 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceYamlFilename); if (workspaceYaml) { - workspaceFile.catalogs = workspaceYaml.catalogs; - workspaceFile.allowBuilds = workspaceYaml.allowBuilds; - workspaceFile.overrides = workspaceYaml.overrides; - workspaceFile.packageExtensions = workspaceYaml.packageExtensions; - workspaceFile.peerDependencyRules = workspaceYaml.peerDependencyRules; - workspaceFile.allowedDeprecatedVersions = workspaceYaml.allowedDeprecatedVersions; - workspaceFile.patchedDependencies = workspaceYaml.patchedDependencies; - workspaceFile.minimumReleaseAge = workspaceYaml.minimumReleaseAge; - workspaceFile.minimumReleaseAgeExclude = workspaceYaml.minimumReleaseAgeExclude; + const { + catalogs, + allowBuilds, + overrides, + packageExtensions, + peerDependencyRules, + allowedDeprecatedVersions, + patchedDependencies, + minimumReleaseAge, + minimumReleaseAgeExclude + } = workspaceYaml; + workspaceFile.catalogs = catalogs; + workspaceFile.allowBuilds = allowBuilds; + workspaceFile.overrides = overrides; + workspaceFile.packageExtensions = packageExtensions; + workspaceFile.peerDependencyRules = peerDependencyRules; + workspaceFile.allowedDeprecatedVersions = allowedDeprecatedVersions; + workspaceFile.patchedDependencies = patchedDependencies; + workspaceFile.minimumReleaseAge = minimumReleaseAge; + workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; } return workspaceFile; 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 32c586c287..8ff14e9dde 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -350,44 +350,56 @@ describe(PnpmWorkspaceFile.name, () => { }); }); - describe(PnpmWorkspaceFile.loadAsync.name, () => { + describe(PnpmWorkspaceFile.tryLoadAsync.name, () => { let mockReadFileAsync: jest.SpyInstance; - beforeEach(() => { - // Mock FileSystem.readFileAsync to return the content captured by the FileSystem.writeFile mock - mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => { - if (writtenContent === undefined) { - throw new Error('File not found'); - } - return writtenContent; + describe('file exists', () => { + beforeEach(() => { + // Mock FileSystem.readFileAsync to return the content captured by the FileSystem.writeFile mock + mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => { + if (writtenContent === undefined) { + throw new Error('File not found'); + } + + return writtenContent; + }); }); - }); - afterEach(() => { - mockReadFileAsync.mockRestore(); - }); + afterEach(() => { + mockReadFileAsync.mockRestore(); + }); - it('reads patchedDependencies from an existing workspace file', async () => { - const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(`${projectsDir}/app1`); - workspaceFile.patchedDependencies = { - 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' - }; - await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + it('reads patchedDependencies from an existing workspace file', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + workspaceFile.patchedDependencies = { + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }; + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + const loadedWorkspaceFile: PnpmWorkspaceFile | undefined = + await PnpmWorkspaceFile.tryLoadAsync(workspaceFilePath); + expect(loadedWorkspaceFile?.patchedDependencies).toEqual({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + }); - const loadedWorkspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(workspaceFilePath); - expect(loadedWorkspaceFile.patchedDependencies).toEqual({ - 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + it('returns undefined when the workspace file has no patchedDependencies', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + const loadedWorkspaceFile: PnpmWorkspaceFile | undefined = + await PnpmWorkspaceFile.tryLoadAsync(workspaceFilePath); + expect(loadedWorkspaceFile!.patchedDependencies).toBeUndefined(); }); }); - it('returns undefined when the workspace file has no patchedDependencies', async () => { - const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); - workspaceFile.addPackage(`${projectsDir}/app1`); - await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); - - const loadedWorkspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(workspaceFilePath); - expect(loadedWorkspaceFile.patchedDependencies).toBeUndefined(); + it("handles the case when the file doesn't exist", async () => { + const loadedWorkspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync( + `${__dirname}/file-that-does-not-exist.yaml` + ); + expect(loadedWorkspaceFile).toBeUndefined(); }); }); From 4e1cf76409b10dd5ea022ad5d4347bfb4fbc602a Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 17:19:54 -0400 Subject: [PATCH 2/7] fixup! [rush] Clean up PNPM options saving (#5874) --- libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 56a22de16b..a80c014117 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -180,9 +180,6 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { } protected override async serializeAsync(): Promise { - // Ensure stable sort order when serializing - Sort.sortSet(this._workspacePackages); - const { _workspacePackages: workspacePackages, catalogs, @@ -195,6 +192,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { minimumReleaseAge, minimumReleaseAgeExclude } = this; + // Ensure stable sort order when serializing + Sort.sortSet(workspacePackages); const workspaceYaml: IPnpmWorkspaceYaml = { packages: Array.from(workspacePackages), // js-yaml omits mapping entries whose value is `undefined`, so no guard is needed here. From 2de20b123eeaa7a212452cd2c587286e87b6b119 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 20:21:53 -0400 Subject: [PATCH 3/7] [rush] Relocate ignoredOptionalDependencies and trustPolicy settings to pnpm-workspace.yaml for pnpm 11 For pnpm 11, these settings were written to the "pnpm" field of the generated common/temp/package.json, which pnpm 11 no longer reads, so they were silently ignored. They now go to common/temp/pnpm-workspace.yaml, matching the other relocated pnpm settings. --- ...-helpers-cleanup2_2026-07-18-00-21-22.json | 11 ++++ .../logic/installManager/InstallHelpers.ts | 19 ++++--- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 52 ++++++++++++++++++- .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 4 ++ .../PnpmWorkspaceFile.test.ts.snap | 6 +++ .../src/logic/test/InstallHelpers.test.ts | 13 +++++ .../common/config/rush/pnpm-config.json | 6 ++- 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json diff --git a/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json b/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json new file mode 100644 index 0000000000..5cf83cef11 --- /dev/null +++ b/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fixed an issue where the pnpm \"ignoredOptionalDependencies\", \"trustPolicy\", \"trustPolicyExclude\", and \"trustPolicyIgnoreAfterMinutes\" settings were written to the \"pnpm\" field of the generated package.json (which pnpm 11 ignores) instead of pnpm-workspace.yaml, causing them to be silently ignored under pnpm 11.", + "type": "patch", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "iclanton@users.noreply.github.com" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 4c74ba1a5d..e92cfb1dbe 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -325,16 +325,13 @@ export class InstallHelpers { workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; // pnpm 11 no longer reads the "pnpm" field of package.json. For pnpm 11+, the overrides, - // packageExtensions, peerDependencyRules, allowedDeprecatedVersions, and patchedDependencies - // settings are written to common/temp/pnpm-workspace.yaml (see workspaceSettings below) instead. + // packageExtensions, peerDependencyRules, allowedDeprecatedVersions, patchedDependencies, + // ignoredOptionalDependencies, and trustPolicy settings are written to + // common/temp/pnpm-workspace.yaml instead. // See https://github.com/microsoft/rushstack/issues/5837 const packageJsonPnpmSection: ICommonPackageJsonPnpmSection = { neverBuiltDependencies, - onlyBuiltDependencies, - ignoredOptionalDependencies: globalIgnoredOptionalDependencies, - trustPolicy, - trustPolicyExclude, - trustPolicyIgnoreAfter + onlyBuiltDependencies }; if (isPnpm11) { @@ -345,12 +342,20 @@ export class InstallHelpers { workspaceFile.peerDependencyRules = globalPeerDependencyRules; workspaceFile.allowedDeprecatedVersions = globalAllowedDeprecatedVersions; workspaceFile.patchedDependencies = globalPatchedDependencies; + workspaceFile.ignoredOptionalDependencies = globalIgnoredOptionalDependencies; + workspaceFile.trustPolicy = trustPolicy; + workspaceFile.trustPolicyExclude = trustPolicyExclude; + workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; } else { packageJsonPnpmSection.overrides = globalOverrides; packageJsonPnpmSection.packageExtensions = globalPackageExtensions; packageJsonPnpmSection.peerDependencyRules = globalPeerDependencyRules; packageJsonPnpmSection.allowedDeprecatedVersions = globalAllowedDeprecatedVersions; packageJsonPnpmSection.patchedDependencies = globalPatchedDependencies; + packageJsonPnpmSection.ignoredOptionalDependencies = globalIgnoredOptionalDependencies; + packageJsonPnpmSection.trustPolicy = trustPolicy; + packageJsonPnpmSection.trustPolicyExclude = trustPolicyExclude; + packageJsonPnpmSection.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; } return { diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index a80c014117..8653b00c52 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -9,7 +9,11 @@ import { FileSystem, Sort, Path } from '@rushstack/node-core-library'; import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile'; import { PNPM_SHRINKWRAP_YAML_FORMAT } from './PnpmYamlCommon'; -import type { IPnpmPackageExtension, IPnpmPeerDependencyRules } from './PnpmOptionsConfiguration'; +import type { + IPnpmPackageExtension, + IPnpmPeerDependencyRules, + PnpmTrustPolicy +} from './PnpmOptionsConfiguration'; /** * This interface represents the raw pnpm-workspace.YAML file @@ -71,6 +75,32 @@ interface IPnpmWorkspaceYaml { * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) */ patchedDependencies: Record | undefined; + /** + * Optional dependencies whose names are listed here are skipped during installation. In pnpm 11+ + * this replaces the `pnpm.ignoredOptionalDependencies` field of `package.json`, which pnpm no + * longer reads. + * (SUPPORTED ONLY IN PNPM 9.0.0 AND NEWER) + */ + ignoredOptionalDependencies: string[] | undefined; + /** + * The trust policy applied when installing packages. In pnpm 11+ this replaces the + * `pnpm.trustPolicy` field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 10.21.0 AND NEWER) + */ + trustPolicy: PnpmTrustPolicy | undefined; + /** + * Package selectors excluded from the trust policy check. In pnpm 11+ this replaces the + * `pnpm.trustPolicyExclude` field of `package.json`, which pnpm no longer reads. + * (SUPPORTED ONLY IN PNPM 10.22.0 AND NEWER) + */ + trustPolicyExclude: string[] | undefined; + /** + * Ignore the trust policy check for packages published more than this many minutes ago. In + * pnpm 11+ this replaces the `pnpm.trustPolicyIgnoreAfter` field of `package.json`, which pnpm + * no longer reads. + * (SUPPORTED ONLY IN PNPM 10.27.0 AND NEWER) + */ + trustPolicyIgnoreAfter: number | undefined; /** * The minimum number of minutes that must pass after a version is published before pnpm will install it. * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) @@ -97,6 +127,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { public peerDependencyRules: IPnpmWorkspaceYaml['peerDependencyRules']; public allowedDeprecatedVersions: IPnpmWorkspaceYaml['allowedDeprecatedVersions']; public patchedDependencies: IPnpmWorkspaceYaml['patchedDependencies']; + public ignoredOptionalDependencies: IPnpmWorkspaceYaml['ignoredOptionalDependencies']; + public trustPolicy: IPnpmWorkspaceYaml['trustPolicy']; + public trustPolicyExclude: IPnpmWorkspaceYaml['trustPolicyExclude']; + public trustPolicyIgnoreAfter: IPnpmWorkspaceYaml['trustPolicyIgnoreAfter']; public minimumReleaseAge: IPnpmWorkspaceYaml['minimumReleaseAge']; public minimumReleaseAgeExclude: IPnpmWorkspaceYaml['minimumReleaseAgeExclude']; @@ -151,6 +185,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { peerDependencyRules, allowedDeprecatedVersions, patchedDependencies, + ignoredOptionalDependencies, + trustPolicy, + trustPolicyExclude, + trustPolicyIgnoreAfter, minimumReleaseAge, minimumReleaseAgeExclude } = workspaceYaml; @@ -161,6 +199,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceFile.peerDependencyRules = peerDependencyRules; workspaceFile.allowedDeprecatedVersions = allowedDeprecatedVersions; workspaceFile.patchedDependencies = patchedDependencies; + workspaceFile.ignoredOptionalDependencies = ignoredOptionalDependencies; + workspaceFile.trustPolicy = trustPolicy; + workspaceFile.trustPolicyExclude = trustPolicyExclude; + workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; workspaceFile.minimumReleaseAge = minimumReleaseAge; workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; } @@ -189,6 +231,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { peerDependencyRules, allowedDeprecatedVersions, patchedDependencies, + ignoredOptionalDependencies, + trustPolicy, + trustPolicyExclude, + trustPolicyIgnoreAfter, minimumReleaseAge, minimumReleaseAgeExclude } = this; @@ -205,6 +251,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { peerDependencyRules, allowedDeprecatedVersions, patchedDependencies, + ignoredOptionalDependencies, + trustPolicy, + trustPolicyExclude, + trustPolicyIgnoreAfter, minimumReleaseAge, minimumReleaseAgeExclude }; 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 8ff14e9dde..87f9da1d54 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -437,6 +437,10 @@ describe(PnpmWorkspaceFile.name, () => { workspaceFile.patchedDependencies = { 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' }; + workspaceFile.ignoredOptionalDependencies = ['fsevents']; + workspaceFile.trustPolicy = 'no-downgrade'; + workspaceFile.trustPolicyExclude = ['chokidar@4.0.3']; + workspaceFile.trustPolicyIgnoreAfter = 1440; await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); 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 0bfb0888ed..d230c6623c 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 @@ -104,6 +104,8 @@ allowedDeprecatedVersions: catalogs: default: react: ^18.0.0 +ignoredOptionalDependencies: + - fsevents overrides: foo@1.0.0: 1.0.1 packageExtensions: @@ -117,6 +119,10 @@ patchedDependencies: peerDependencyRules: allowedVersions: react: '18' +trustPolicy: no-downgrade +trustPolicyExclude: + - chokidar@4.0.3 +trustPolicyIgnoreAfter: 1440 " `; diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index b766db92f1..fdec20f42b 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -6,6 +6,7 @@ import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; import { InstallHelpers } from '../installManager/InstallHelpers'; import { RushConfiguration } from '../../api/RushConfiguration'; +import type { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile'; describe(InstallHelpers.name, () => { describe(InstallHelpers.generateCommonPackageJsonAsync.name, () => { @@ -112,6 +113,18 @@ describe(InstallHelpers.name, () => { expect(pnpmField).not.toHaveProperty('peerDependencyRules'); expect(pnpmField).not.toHaveProperty('allowedDeprecatedVersions'); expect(pnpmField).not.toHaveProperty('patchedDependencies'); + expect(pnpmField).not.toHaveProperty('ignoredOptionalDependencies'); + expect(pnpmField).not.toHaveProperty('trustPolicy'); + expect(pnpmField).not.toHaveProperty('trustPolicyExclude'); + expect(pnpmField).not.toHaveProperty('trustPolicyIgnoreAfter'); + + // ...and the newly relocated settings are instead placed on the generated pnpm-workspace.yaml + // file. (The arrays are spread to drop the ConfigurationFile annotation symbol they carry.) + const workspaceFile: PnpmWorkspaceFile = pnpmSettings!.workspaceFile; + expect([...(workspaceFile.ignoredOptionalDependencies ?? [])]).toEqual(['fsevents']); + expect(workspaceFile.trustPolicy).toEqual('no-downgrade'); + expect([...(workspaceFile.trustPolicyExclude ?? [])]).toEqual(['chokidar@4.0.3']); + expect(workspaceFile.trustPolicyIgnoreAfter).toEqual(1440); }); }); }); 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 index 0c863d5ac5..16aecf97bf 100644 --- 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 @@ -21,5 +21,9 @@ }, "globalPatchedDependencies": { "lodash@4.17.21": "patches/lodash@4.17.21.patch" - } + }, + "globalIgnoredOptionalDependencies": ["fsevents"], + "trustPolicy": "no-downgrade", + "trustPolicyExclude": ["chokidar@4.0.3"], + "trustPolicyIgnoreAfterMinutes": 1440 } From ed18f0c5684122359a8ee6b3a2f926e38bbdf381 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 20:29:35 -0400 Subject: [PATCH 4/7] For pnpm 11, omit the package.json "pnpm" field entirely Since pnpm 11 relocates every setting to pnpm-workspace.yaml, the generated package.json "pnpm" field was always empty; don't emit it at all for pnpm 11. --- .../logic/installManager/InstallHelpers.ts | 42 +++++++++---------- .../src/logic/test/InstallHelpers.test.ts | 21 +++------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index e92cfb1dbe..1f8e30251d 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -48,10 +48,10 @@ interface ICommonPackageJson extends IPackageJson { */ export interface IResolvedPnpmSettings { /** - * The "pnpm" field to write into `common/temp/package.json`. For pnpm 11+, settings that pnpm - * no longer reads from package.json are omitted here and appear in {@link workspaceFile}. + * The "pnpm" field to write into `common/temp/package.json`, or `undefined` for pnpm 11+ (which + * no longer reads that field — all settings are placed on {@link workspaceFile} instead). */ - packageJsonPnpmSection: ICommonPackageJsonPnpmSection; + packageJsonPnpmSection: ICommonPackageJsonPnpmSection | undefined; /** * Additional top-level properties to merge into `common/temp/package.json` @@ -324,19 +324,12 @@ export class InstallHelpers { workspaceFile.minimumReleaseAge = minimumReleaseAgeMinutes; workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude; - // pnpm 11 no longer reads the "pnpm" field of package.json. For pnpm 11+, the overrides, - // packageExtensions, peerDependencyRules, allowedDeprecatedVersions, patchedDependencies, - // ignoredOptionalDependencies, and trustPolicy settings are written to - // common/temp/pnpm-workspace.yaml instead. + // pnpm 11 no longer reads the "pnpm" field of package.json, so for pnpm 11+ we don't generate + // it at all; every setting is written to common/temp/pnpm-workspace.yaml instead. // See https://github.com/microsoft/rushstack/issues/5837 - const packageJsonPnpmSection: ICommonPackageJsonPnpmSection = { - neverBuiltDependencies, - onlyBuiltDependencies - }; + let packageJsonPnpmSection: ICommonPackageJsonPnpmSection | undefined; if (isPnpm11) { - // These are written to pnpm-workspace.yaml only for pnpm 11+ (see note above); for older pnpm - // they live in packageJsonPnpmSection instead and are left undefined here. workspaceFile.overrides = globalOverrides; workspaceFile.packageExtensions = globalPackageExtensions; workspaceFile.peerDependencyRules = globalPeerDependencyRules; @@ -347,15 +340,20 @@ export class InstallHelpers { workspaceFile.trustPolicyExclude = trustPolicyExclude; workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; } else { - packageJsonPnpmSection.overrides = globalOverrides; - packageJsonPnpmSection.packageExtensions = globalPackageExtensions; - packageJsonPnpmSection.peerDependencyRules = globalPeerDependencyRules; - packageJsonPnpmSection.allowedDeprecatedVersions = globalAllowedDeprecatedVersions; - packageJsonPnpmSection.patchedDependencies = globalPatchedDependencies; - packageJsonPnpmSection.ignoredOptionalDependencies = globalIgnoredOptionalDependencies; - packageJsonPnpmSection.trustPolicy = trustPolicy; - packageJsonPnpmSection.trustPolicyExclude = trustPolicyExclude; - packageJsonPnpmSection.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter; + // For older pnpm, these settings live in the "pnpm" field of package.json. + packageJsonPnpmSection = { + neverBuiltDependencies, + onlyBuiltDependencies, + overrides: globalOverrides, + packageExtensions: globalPackageExtensions, + peerDependencyRules: globalPeerDependencyRules, + allowedDeprecatedVersions: globalAllowedDeprecatedVersions, + patchedDependencies: globalPatchedDependencies, + ignoredOptionalDependencies: globalIgnoredOptionalDependencies, + trustPolicy, + trustPolicyExclude, + trustPolicyIgnoreAfter + }; } return { diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index fdec20f42b..9c94f63a1a 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -88,7 +88,7 @@ describe(InstallHelpers.name, () => { expect(packageJson).toMatchSnapshot(); }); - it('omits the relocated pnpm settings for pnpm 11 (they belong in pnpm-workspace.yaml)', async () => { + it('does not generate a "pnpm" field for pnpm 11 (all settings belong in pnpm-workspace.yaml)', async () => { const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfigPnpm11/rush.json`; const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); @@ -105,21 +105,12 @@ describe(InstallHelpers.name, () => { const packageJson: IPackageJson = JSON.parse( JsonFile.stringify(mockJsonFileSaveAsync.mock.calls[0][0], { ignoreUndefinedValues: true }) ); - const pnpmField: Record = (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'); - expect(pnpmField).not.toHaveProperty('ignoredOptionalDependencies'); - expect(pnpmField).not.toHaveProperty('trustPolicy'); - expect(pnpmField).not.toHaveProperty('trustPolicyExclude'); - expect(pnpmField).not.toHaveProperty('trustPolicyIgnoreAfter'); + // For pnpm >= 11 the "pnpm" field is not generated at all; every setting is written to + // common/temp/pnpm-workspace.yaml instead. + expect(packageJson).not.toHaveProperty('pnpm'); - // ...and the newly relocated settings are instead placed on the generated pnpm-workspace.yaml - // file. (The arrays are spread to drop the ConfigurationFile annotation symbol they carry.) + // ...and the relocated settings are instead placed on the generated pnpm-workspace.yaml file. + // (The arrays are spread to drop the ConfigurationFile annotation symbol they carry.) const workspaceFile: PnpmWorkspaceFile = pnpmSettings!.workspaceFile; expect([...(workspaceFile.ignoredOptionalDependencies ?? [])]).toEqual(['fsevents']); expect(workspaceFile.trustPolicy).toEqual('no-downgrade'); From cb2eca6057b6185f06b6179862c9bde11592fb6f Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 20:42:03 -0400 Subject: [PATCH 5/7] Update InstallHelpers test snapshot for pnpm 11 test rename --- .../src/logic/test/__snapshots__/InstallHelpers.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 c453f9719c..c9b8a524b3 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,5 +1,7 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +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 generates correct package json with pnpm configurations 1`] = ` Object { "dependencies": Object {}, @@ -49,5 +51,3 @@ Object { `; exports[`InstallHelpers generateCommonPackageJsonAsync generates correct package json with pnpm configurations: Terminal Output 1`] = `Array []`; - -exports[`InstallHelpers generateCommonPackageJsonAsync omits the relocated pnpm settings for pnpm 11 (they belong in pnpm-workspace.yaml): Terminal Output 1`] = `Array []`; From 8f7b9ea213cce2458070df4f15938e31782ca941 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 20:51:41 -0400 Subject: [PATCH 6/7] Rush change. --- .../rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json b/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json index 5cf83cef11..e96df99cb6 100644 --- a/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json +++ b/common/changes/@microsoft/rush/more-install-helpers-cleanup2_2026-07-18-00-21-22.json @@ -1,7 +1,7 @@ { "changes": [ { - "comment": "Fixed an issue where the pnpm \"ignoredOptionalDependencies\", \"trustPolicy\", \"trustPolicyExclude\", and \"trustPolicyIgnoreAfterMinutes\" settings were written to the \"pnpm\" field of the generated package.json (which pnpm 11 ignores) instead of pnpm-workspace.yaml, causing them to be silently ignored under pnpm 11.", + "comment": "Fixed an issue where the pnpm `ignoredOptionalDependencies`, `trustPolicy`, `trustPolicyExclude`, and `trustPolicyIgnoreAfterMinutes` settings were written to the `pnpm` field of the generated `package.json` (which pnpm 11 ignores) instead of `pnpm-workspace.yaml`, causing them to be silently ignored under pnpm 11.", "type": "patch", "packageName": "@microsoft/rush" } From 1bbaa3d4aacb565bd4743972038145cd2f450ba0 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Fri, 17 Jul 2026 21:16:18 -0400 Subject: [PATCH 7/7] Use TestUtilities.stripAnnotations in the InstallHelpers pnpm 11 test --- .../rush-lib/src/logic/test/InstallHelpers.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts index 9c94f63a1a..15a8d90969 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -3,6 +3,7 @@ import { type IPackageJson, JsonFile } from '@rushstack/node-core-library'; import { StringBufferTerminalProvider, Terminal } from '@rushstack/terminal'; +import { TestUtilities } from '@rushstack/heft-config-file'; import { InstallHelpers } from '../installManager/InstallHelpers'; import { RushConfiguration } from '../../api/RushConfiguration'; @@ -110,12 +111,12 @@ describe(InstallHelpers.name, () => { expect(packageJson).not.toHaveProperty('pnpm'); // ...and the relocated settings are instead placed on the generated pnpm-workspace.yaml file. - // (The arrays are spread to drop the ConfigurationFile annotation symbol they carry.) - const workspaceFile: PnpmWorkspaceFile = pnpmSettings!.workspaceFile; - expect([...(workspaceFile.ignoredOptionalDependencies ?? [])]).toEqual(['fsevents']); - expect(workspaceFile.trustPolicy).toEqual('no-downgrade'); - expect([...(workspaceFile.trustPolicyExclude ?? [])]).toEqual(['chokidar@4.0.3']); - expect(workspaceFile.trustPolicyIgnoreAfter).toEqual(1440); + const workspaceFile: PnpmWorkspaceFile | undefined = + TestUtilities.stripAnnotations(pnpmSettings)?.workspaceFile; + expect(workspaceFile?.ignoredOptionalDependencies).toEqual(['fsevents']); + expect(workspaceFile?.trustPolicy).toEqual('no-downgrade'); + expect(workspaceFile?.trustPolicyExclude).toEqual(['chokidar@4.0.3']); + expect(workspaceFile?.trustPolicyIgnoreAfter).toEqual(1440); }); }); });