Skip to content

Commit b095090

Browse files
authored
[rush] Relocate remaining pnpm settings to pnpm-workspace.yaml for pnpm 11 (#5882)
* 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. * fixup! [rush] Clean up PNPM options saving (#5874) * [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. * 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. * Update InstallHelpers test snapshot for pnpm 11 test rename
1 parent 3225b82 commit b095090

9 files changed

Lines changed: 196 additions & 81 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"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.",
5+
"type": "patch",
6+
"packageName": "@microsoft/rush"
7+
}
8+
],
9+
"packageName": "@microsoft/rush",
10+
"email": "iclanton@users.noreply.github.com"
11+
}

libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -545,10 +545,10 @@ export class RushPnpmCommandLineParser {
545545
let newGlobalPatchedDependencies: Record<string, string> | undefined;
546546
if (semver.gte(pnpmVersion, '11.0.0')) {
547547
// PNPM 11+ stores patchedDependencies in pnpm-workspace.yaml instead of the package.json "pnpm" field
548-
const workspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(
548+
const workspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync(
549549
`${subspaceTempFolder}/${RushConstants.pnpmWorkspaceFileName}`
550550
);
551-
newGlobalPatchedDependencies = workspaceFile.patchedDependencies;
551+
newGlobalPatchedDependencies = workspaceFile?.patchedDependencies;
552552
} else {
553553
// PNPM 10.x and earlier store patchedDependencies in the package.json "pnpm" field
554554
// Example: "C:\MyRepo\common\temp\package.json"
@@ -613,10 +613,10 @@ export class RushPnpmCommandLineParser {
613613

614614
if (semver.gte(pnpmVersion, '11.0.0')) {
615615
// PNPM 11+ uses allowBuilds in pnpm-workspace.yaml instead of onlyBuiltDependencies in package.json
616-
const workspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(
616+
const workspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync(
617617
`${subspaceTempFolder}/${RushConstants.pnpmWorkspaceFileName}`
618618
);
619-
const newGlobalAllowBuilds: Record<string, boolean> | undefined = workspaceFile.allowBuilds;
619+
const newGlobalAllowBuilds: Record<string, boolean> | undefined = workspaceFile?.allowBuilds;
620620
const currentGlobalAllowBuilds: Record<string, boolean> | undefined =
621621
pnpmOptions?.globalAllowBuilds;
622622

libraries/rush-lib/src/logic/installManager/InstallHelpers.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ interface ICommonPackageJson extends IPackageJson {
4848
*/
4949
export interface IResolvedPnpmSettings {
5050
/**
51-
* The "pnpm" field to write into `common/temp/package.json`. For pnpm 11+, settings that pnpm
52-
* no longer reads from package.json are omitted here and appear in {@link workspaceFile}.
51+
* The "pnpm" field to write into `common/temp/package.json`, or `undefined` for pnpm 11+ (which
52+
* no longer reads that field — all settings are placed on {@link workspaceFile} instead).
5353
*/
54-
packageJsonPnpmSection: ICommonPackageJsonPnpmSection;
54+
packageJsonPnpmSection: ICommonPackageJsonPnpmSection | undefined;
5555

5656
/**
5757
* Additional top-level properties to merge into `common/temp/package.json`
@@ -324,33 +324,36 @@ export class InstallHelpers {
324324
workspaceFile.minimumReleaseAge = minimumReleaseAgeMinutes;
325325
workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude;
326326

327-
// pnpm 11 no longer reads the "pnpm" field of package.json. For pnpm 11+, the overrides,
328-
// packageExtensions, peerDependencyRules, allowedDeprecatedVersions, and patchedDependencies
329-
// settings are written to common/temp/pnpm-workspace.yaml (see workspaceSettings below) instead.
327+
// pnpm 11 no longer reads the "pnpm" field of package.json, so for pnpm 11+ we don't generate
328+
// it at all; every setting is written to common/temp/pnpm-workspace.yaml instead.
330329
// See https://github.com/microsoft/rushstack/issues/5837
331-
const packageJsonPnpmSection: ICommonPackageJsonPnpmSection = {
332-
neverBuiltDependencies,
333-
onlyBuiltDependencies,
334-
ignoredOptionalDependencies: globalIgnoredOptionalDependencies,
335-
trustPolicy,
336-
trustPolicyExclude,
337-
trustPolicyIgnoreAfter
338-
};
330+
let packageJsonPnpmSection: ICommonPackageJsonPnpmSection | undefined;
339331

340332
if (isPnpm11) {
341-
// These are written to pnpm-workspace.yaml only for pnpm 11+ (see note above); for older pnpm
342-
// they live in packageJsonPnpmSection instead and are left undefined here.
343333
workspaceFile.overrides = globalOverrides;
344334
workspaceFile.packageExtensions = globalPackageExtensions;
345335
workspaceFile.peerDependencyRules = globalPeerDependencyRules;
346336
workspaceFile.allowedDeprecatedVersions = globalAllowedDeprecatedVersions;
347337
workspaceFile.patchedDependencies = globalPatchedDependencies;
338+
workspaceFile.ignoredOptionalDependencies = globalIgnoredOptionalDependencies;
339+
workspaceFile.trustPolicy = trustPolicy;
340+
workspaceFile.trustPolicyExclude = trustPolicyExclude;
341+
workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter;
348342
} else {
349-
packageJsonPnpmSection.overrides = globalOverrides;
350-
packageJsonPnpmSection.packageExtensions = globalPackageExtensions;
351-
packageJsonPnpmSection.peerDependencyRules = globalPeerDependencyRules;
352-
packageJsonPnpmSection.allowedDeprecatedVersions = globalAllowedDeprecatedVersions;
353-
packageJsonPnpmSection.patchedDependencies = globalPatchedDependencies;
343+
// For older pnpm, these settings live in the "pnpm" field of package.json.
344+
packageJsonPnpmSection = {
345+
neverBuiltDependencies,
346+
onlyBuiltDependencies,
347+
overrides: globalOverrides,
348+
packageExtensions: globalPackageExtensions,
349+
peerDependencyRules: globalPeerDependencyRules,
350+
allowedDeprecatedVersions: globalAllowedDeprecatedVersions,
351+
patchedDependencies: globalPatchedDependencies,
352+
ignoredOptionalDependencies: globalIgnoredOptionalDependencies,
353+
trustPolicy,
354+
trustPolicyExclude,
355+
trustPolicyIgnoreAfter
356+
};
354357
}
355358

356359
return {

libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { FileSystem, Sort, Path } from '@rushstack/node-core-library';
99

1010
import { BaseWorkspaceFile } from '../base/BaseWorkspaceFile';
1111
import { PNPM_SHRINKWRAP_YAML_FORMAT } from './PnpmYamlCommon';
12-
import type { IPnpmPackageExtension, IPnpmPeerDependencyRules } from './PnpmOptionsConfiguration';
12+
import type {
13+
IPnpmPackageExtension,
14+
IPnpmPeerDependencyRules,
15+
PnpmTrustPolicy
16+
} from './PnpmOptionsConfiguration';
1317

1418
/**
1519
* This interface represents the raw pnpm-workspace.YAML file
@@ -71,6 +75,32 @@ interface IPnpmWorkspaceYaml {
7175
* (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER)
7276
*/
7377
patchedDependencies: Record<string, string> | undefined;
78+
/**
79+
* Optional dependencies whose names are listed here are skipped during installation. In pnpm 11+
80+
* this replaces the `pnpm.ignoredOptionalDependencies` field of `package.json`, which pnpm no
81+
* longer reads.
82+
* (SUPPORTED ONLY IN PNPM 9.0.0 AND NEWER)
83+
*/
84+
ignoredOptionalDependencies: string[] | undefined;
85+
/**
86+
* The trust policy applied when installing packages. In pnpm 11+ this replaces the
87+
* `pnpm.trustPolicy` field of `package.json`, which pnpm no longer reads.
88+
* (SUPPORTED ONLY IN PNPM 10.21.0 AND NEWER)
89+
*/
90+
trustPolicy: PnpmTrustPolicy | undefined;
91+
/**
92+
* Package selectors excluded from the trust policy check. In pnpm 11+ this replaces the
93+
* `pnpm.trustPolicyExclude` field of `package.json`, which pnpm no longer reads.
94+
* (SUPPORTED ONLY IN PNPM 10.22.0 AND NEWER)
95+
*/
96+
trustPolicyExclude: string[] | undefined;
97+
/**
98+
* Ignore the trust policy check for packages published more than this many minutes ago. In
99+
* pnpm 11+ this replaces the `pnpm.trustPolicyIgnoreAfter` field of `package.json`, which pnpm
100+
* no longer reads.
101+
* (SUPPORTED ONLY IN PNPM 10.27.0 AND NEWER)
102+
*/
103+
trustPolicyIgnoreAfter: number | undefined;
74104
/**
75105
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
76106
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
@@ -97,6 +127,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
97127
public peerDependencyRules: IPnpmWorkspaceYaml['peerDependencyRules'];
98128
public allowedDeprecatedVersions: IPnpmWorkspaceYaml['allowedDeprecatedVersions'];
99129
public patchedDependencies: IPnpmWorkspaceYaml['patchedDependencies'];
130+
public ignoredOptionalDependencies: IPnpmWorkspaceYaml['ignoredOptionalDependencies'];
131+
public trustPolicy: IPnpmWorkspaceYaml['trustPolicy'];
132+
public trustPolicyExclude: IPnpmWorkspaceYaml['trustPolicyExclude'];
133+
public trustPolicyIgnoreAfter: IPnpmWorkspaceYaml['trustPolicyIgnoreAfter'];
100134
public minimumReleaseAge: IPnpmWorkspaceYaml['minimumReleaseAge'];
101135
public minimumReleaseAgeExclude: IPnpmWorkspaceYaml['minimumReleaseAgeExclude'];
102136

@@ -124,24 +158,53 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
124158
*
125159
* @param workspaceYamlFilename - The path to the `pnpm-workspace.yaml` file
126160
*/
127-
public static async loadAsync(workspaceYamlFilename: string): Promise<PnpmWorkspaceFile> {
128-
const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename);
161+
public static async tryLoadAsync(workspaceYamlFilename: string): Promise<PnpmWorkspaceFile | undefined> {
162+
let workspaceYamlContent: string;
163+
try {
164+
workspaceYamlContent = await FileSystem.readFileAsync(workspaceYamlFilename);
165+
} catch (error) {
166+
if (FileSystem.isNotExistError(error)) {
167+
return undefined;
168+
} else {
169+
throw error;
170+
}
171+
}
172+
129173
const yamlModule: typeof import('js-yaml') = await import('js-yaml');
130174
const workspaceYaml: IPnpmWorkspaceYaml | undefined = yamlModule.load(workspaceYamlContent) as
131175
| IPnpmWorkspaceYaml
132176
| undefined;
133177

134178
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceYamlFilename);
135179
if (workspaceYaml) {
136-
workspaceFile.catalogs = workspaceYaml.catalogs;
137-
workspaceFile.allowBuilds = workspaceYaml.allowBuilds;
138-
workspaceFile.overrides = workspaceYaml.overrides;
139-
workspaceFile.packageExtensions = workspaceYaml.packageExtensions;
140-
workspaceFile.peerDependencyRules = workspaceYaml.peerDependencyRules;
141-
workspaceFile.allowedDeprecatedVersions = workspaceYaml.allowedDeprecatedVersions;
142-
workspaceFile.patchedDependencies = workspaceYaml.patchedDependencies;
143-
workspaceFile.minimumReleaseAge = workspaceYaml.minimumReleaseAge;
144-
workspaceFile.minimumReleaseAgeExclude = workspaceYaml.minimumReleaseAgeExclude;
180+
const {
181+
catalogs,
182+
allowBuilds,
183+
overrides,
184+
packageExtensions,
185+
peerDependencyRules,
186+
allowedDeprecatedVersions,
187+
patchedDependencies,
188+
ignoredOptionalDependencies,
189+
trustPolicy,
190+
trustPolicyExclude,
191+
trustPolicyIgnoreAfter,
192+
minimumReleaseAge,
193+
minimumReleaseAgeExclude
194+
} = workspaceYaml;
195+
workspaceFile.catalogs = catalogs;
196+
workspaceFile.allowBuilds = allowBuilds;
197+
workspaceFile.overrides = overrides;
198+
workspaceFile.packageExtensions = packageExtensions;
199+
workspaceFile.peerDependencyRules = peerDependencyRules;
200+
workspaceFile.allowedDeprecatedVersions = allowedDeprecatedVersions;
201+
workspaceFile.patchedDependencies = patchedDependencies;
202+
workspaceFile.ignoredOptionalDependencies = ignoredOptionalDependencies;
203+
workspaceFile.trustPolicy = trustPolicy;
204+
workspaceFile.trustPolicyExclude = trustPolicyExclude;
205+
workspaceFile.trustPolicyIgnoreAfter = trustPolicyIgnoreAfter;
206+
workspaceFile.minimumReleaseAge = minimumReleaseAge;
207+
workspaceFile.minimumReleaseAgeExclude = minimumReleaseAgeExclude;
145208
}
146209

147210
return workspaceFile;
@@ -159,9 +222,6 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
159222
}
160223

161224
protected override async serializeAsync(): Promise<string> {
162-
// Ensure stable sort order when serializing
163-
Sort.sortSet(this._workspacePackages);
164-
165225
const {
166226
_workspacePackages: workspacePackages,
167227
catalogs,
@@ -171,9 +231,15 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
171231
peerDependencyRules,
172232
allowedDeprecatedVersions,
173233
patchedDependencies,
234+
ignoredOptionalDependencies,
235+
trustPolicy,
236+
trustPolicyExclude,
237+
trustPolicyIgnoreAfter,
174238
minimumReleaseAge,
175239
minimumReleaseAgeExclude
176240
} = this;
241+
// Ensure stable sort order when serializing
242+
Sort.sortSet(workspacePackages);
177243
const workspaceYaml: IPnpmWorkspaceYaml = {
178244
packages: Array.from(workspacePackages),
179245
// js-yaml omits mapping entries whose value is `undefined`, so no guard is needed here.
@@ -185,6 +251,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
185251
peerDependencyRules,
186252
allowedDeprecatedVersions,
187253
patchedDependencies,
254+
ignoredOptionalDependencies,
255+
trustPolicy,
256+
trustPolicyExclude,
257+
trustPolicyIgnoreAfter,
188258
minimumReleaseAge,
189259
minimumReleaseAgeExclude
190260
};

libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -350,44 +350,56 @@ describe(PnpmWorkspaceFile.name, () => {
350350
});
351351
});
352352

353-
describe(PnpmWorkspaceFile.loadAsync.name, () => {
353+
describe(PnpmWorkspaceFile.tryLoadAsync.name, () => {
354354
let mockReadFileAsync: jest.SpyInstance;
355355

356-
beforeEach(() => {
357-
// Mock FileSystem.readFileAsync to return the content captured by the FileSystem.writeFile mock
358-
mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => {
359-
if (writtenContent === undefined) {
360-
throw new Error('File not found');
361-
}
362-
return writtenContent;
356+
describe('file exists', () => {
357+
beforeEach(() => {
358+
// Mock FileSystem.readFileAsync to return the content captured by the FileSystem.writeFile mock
359+
mockReadFileAsync = jest.spyOn(FileSystem, 'readFileAsync').mockImplementation(async () => {
360+
if (writtenContent === undefined) {
361+
throw new Error('File not found');
362+
}
363+
364+
return writtenContent;
365+
});
363366
});
364-
});
365367

366-
afterEach(() => {
367-
mockReadFileAsync.mockRestore();
368-
});
368+
afterEach(() => {
369+
mockReadFileAsync.mockRestore();
370+
});
369371

370-
it('reads patchedDependencies from an existing workspace file', async () => {
371-
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
372-
workspaceFile.addPackage(`${projectsDir}/app1`);
373-
workspaceFile.patchedDependencies = {
374-
'lodash@4.17.21': 'patches/lodash@4.17.21.patch'
375-
};
376-
await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true });
372+
it('reads patchedDependencies from an existing workspace file', async () => {
373+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
374+
workspaceFile.addPackage(`${projectsDir}/app1`);
375+
workspaceFile.patchedDependencies = {
376+
'lodash@4.17.21': 'patches/lodash@4.17.21.patch'
377+
};
378+
await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true });
379+
380+
const loadedWorkspaceFile: PnpmWorkspaceFile | undefined =
381+
await PnpmWorkspaceFile.tryLoadAsync(workspaceFilePath);
382+
expect(loadedWorkspaceFile?.patchedDependencies).toEqual({
383+
'lodash@4.17.21': 'patches/lodash@4.17.21.patch'
384+
});
385+
});
377386

378-
const loadedWorkspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(workspaceFilePath);
379-
expect(loadedWorkspaceFile.patchedDependencies).toEqual({
380-
'lodash@4.17.21': 'patches/lodash@4.17.21.patch'
387+
it('returns undefined when the workspace file has no patchedDependencies', async () => {
388+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
389+
workspaceFile.addPackage(`${projectsDir}/app1`);
390+
await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true });
391+
392+
const loadedWorkspaceFile: PnpmWorkspaceFile | undefined =
393+
await PnpmWorkspaceFile.tryLoadAsync(workspaceFilePath);
394+
expect(loadedWorkspaceFile!.patchedDependencies).toBeUndefined();
381395
});
382396
});
383397

384-
it('returns undefined when the workspace file has no patchedDependencies', async () => {
385-
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
386-
workspaceFile.addPackage(`${projectsDir}/app1`);
387-
await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true });
388-
389-
const loadedWorkspaceFile: PnpmWorkspaceFile = await PnpmWorkspaceFile.loadAsync(workspaceFilePath);
390-
expect(loadedWorkspaceFile.patchedDependencies).toBeUndefined();
398+
it("handles the case when the file doesn't exist", async () => {
399+
const loadedWorkspaceFile: PnpmWorkspaceFile | undefined = await PnpmWorkspaceFile.tryLoadAsync(
400+
`${__dirname}/file-that-does-not-exist.yaml`
401+
);
402+
expect(loadedWorkspaceFile).toBeUndefined();
391403
});
392404
});
393405

@@ -425,6 +437,10 @@ describe(PnpmWorkspaceFile.name, () => {
425437
workspaceFile.patchedDependencies = {
426438
'lodash@4.17.21': 'patches/lodash@4.17.21.patch'
427439
};
440+
workspaceFile.ignoredOptionalDependencies = ['fsevents'];
441+
workspaceFile.trustPolicy = 'no-downgrade';
442+
workspaceFile.trustPolicyExclude = ['chokidar@4.0.3'];
443+
workspaceFile.trustPolicyIgnoreAfter = 1440;
428444

429445
await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true });
430446

libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ allowedDeprecatedVersions:
104104
catalogs:
105105
default:
106106
react: ^18.0.0
107+
ignoredOptionalDependencies:
108+
- fsevents
107109
overrides:
108110
foo@1.0.0: 1.0.1
109111
packageExtensions:
@@ -117,6 +119,10 @@ patchedDependencies:
117119
peerDependencyRules:
118120
allowedVersions:
119121
react: '18'
122+
trustPolicy: no-downgrade
123+
trustPolicyExclude:
124+
- chokidar@4.0.3
125+
trustPolicyIgnoreAfter: 1440
120126
"
121127
`;
122128

0 commit comments

Comments
 (0)