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..083c8421d9 --- /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), 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" + } + ], + "packageName": "@microsoft/rush", + "email": "brunojppb@users.noreply.github.com" +} 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/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 +" +`; 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": [] +}