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..9a83b272d9 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -32,6 +32,7 @@ import type { IInstallManagerOptions } from '../logic/base/BaseInstallManagerTyp import { Utilities } from '../utilities/Utilities'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; +import { PnpmWorkspaceFile } from '../logic/pnpm/PnpmWorkspaceFile'; import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; import { initializeDotEnv } from '../logic/dotenv'; @@ -537,12 +538,24 @@ 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 + newGlobalPatchedDependencies = await PnpmWorkspaceFile.loadPatchedDependenciesAsync( + `${subspaceTempFolder}/pnpm-workspace.yaml` + ); + } 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..035e58c917 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,34 @@ async function validatePnpmArgsAsync(pnpmArgs: string[]): Promise { return pnpmArgs; } +const SUBSPACE_TEMP_FOLDER: string = '/repo/common/temp'; + +function createPostExecuteParser(options: { + commandName: string; + pnpmVersion: string; + globalPatchedDependencies: Record | undefined; + updateGlobalPatchedDependencies: jest.Mock; + doRushUpdateAsync: jest.Mock; +}): RushPnpmCommandLineParser { + const parser: RushPnpmCommandLineParser = Object.create(RushPnpmCommandLineParser.prototype); + Object.assign(parser, { + _commandName: options.commandName, + _rushConfiguration: { packageManagerToolVersion: options.pnpmVersion }, + _terminal: { writeWarningLine: jest.fn(), writeErrorLine: jest.fn() }, + _doRushUpdateAsync: options.doRushUpdateAsync, + _subspace: { + getSubspaceTempFolderPath: () => SUBSPACE_TEMP_FOLDER, + getSubspaceConfigFolderPath: () => '/repo/common/config/rush', + getSubspacePnpmPatchesFolderPath: () => '/repo/common/config/rush/pnpm-patches', + getPnpmOptions: () => ({ + globalPatchedDependencies: options.globalPatchedDependencies, + updateGlobalPatchedDependencies: options.updateGlobalPatchedDependencies + }) + } + }); + 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 +64,65 @@ describe(RushPnpmCommandLineParser.name, () => { await expect(validatePnpmArgsAsync(['outdated', '--global'])).resolves.toEqual(['outdated', '--global']); }); }); + +describe(`${RushPnpmCommandLineParser.name} patch-commit patchedDependencies sync`, () => { + it('reads patchedDependencies from pnpm-workspace.yaml for pnpm >= 11', async () => { + const updateGlobalPatchedDependencies: jest.Mock = jest.fn(); + const doRushUpdateAsync: jest.Mock = jest.fn(); + const parser: RushPnpmCommandLineParser = createPostExecuteParser({ + commandName: 'patch-commit', + pnpmVersion: '11.7.0', + globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, + updateGlobalPatchedDependencies, + doRushUpdateAsync + }); + + const workspaceYaml: string = + 'packages:\n' + + ' - ../../app\n' + + 'patchedDependencies:\n' + + ' lodash@4.17.21: patches/lodash@4.17.21.patch\n'; + const readFileAsyncSpy: jest.SpyInstance = jest + .spyOn(FileSystem, 'readFileAsync') + .mockResolvedValue(workspaceYaml); + // If the code incorrectly read package.json for pnpm 11, it would pick up this sentinel value. + const jsonLoadSpy: jest.SpyInstance = 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(updateGlobalPatchedDependencies).toHaveBeenCalledWith({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + expect(doRushUpdateAsync).toHaveBeenCalledTimes(1); + }); + + it('reads patchedDependencies from package.json for pnpm < 11', async () => { + const updateGlobalPatchedDependencies: jest.Mock = jest.fn(); + const doRushUpdateAsync: jest.Mock = jest.fn(); + const parser: RushPnpmCommandLineParser = createPostExecuteParser({ + commandName: 'patch-commit', + pnpmVersion: '10.27.0', + globalPatchedDependencies: { 'left-pad@1.0.0': 'patches/left-pad@1.0.0.patch' }, + updateGlobalPatchedDependencies, + doRushUpdateAsync + }); + + const readFileAsyncSpy: jest.SpyInstance = jest.spyOn(FileSystem, 'readFileAsync').mockResolvedValue(''); + const jsonLoadSpy: jest.SpyInstance = 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(updateGlobalPatchedDependencies).toHaveBeenCalledWith({ + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }); + expect(doRushUpdateAsync).toHaveBeenCalledTimes(1); + }); +}); diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index a46610a792..30fcdd73f6 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -159,15 +159,18 @@ export class InstallHelpers { additionalCommonPackageJsonPropertiesToMerge = unsupportedPackageJsonSettings; + // 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 pnpmSection = { - overrides, - packageExtensions, - peerDependencyRules, + overrides: isPnpm11 ? undefined : overrides, + packageExtensions: isPnpm11 ? undefined : packageExtensions, + peerDependencyRules: isPnpm11 ? undefined : peerDependencyRules, neverBuiltDependencies, onlyBuiltDependencies, ignoredOptionalDependencies, - allowedDeprecatedVersions, - patchedDependencies, + allowedDeprecatedVersions: isPnpm11 ? undefined : allowedDeprecatedVersions, + patchedDependencies: isPnpm11 ? undefined : patchedDependencies, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index f708068f3d..60ce094f28 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -509,6 +509,22 @@ 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') + ) { + const pnpmOptions: PnpmOptionsConfiguration = + subspace.getPnpmOptions() || this.rushConfiguration.pnpmOptions; + workspaceFile.overrides = pnpmOptions.globalOverrides; + workspaceFile.packageExtensions = pnpmOptions.globalPackageExtensions; + workspaceFile.peerDependencyRules = pnpmOptions.globalPeerDependencyRules; + workspaceFile.allowedDeprecatedVersions = pnpmOptions.globalAllowedDeprecatedVersions; + workspaceFile.patchedDependencies = pnpmOptions.globalPatchedDependencies; + } + // Set minimumReleaseAge/minimumReleaseAgeExclude in the workspace file. // pnpm does not read these fields from package.json, only from pnpm-workspace.yaml or .npmrc. if (minimumReleaseAgeMinutes !== undefined || minimumReleaseAgeExclude) { diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 0be4e009de..9713a37c54 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -5,10 +5,11 @@ import * as path from 'node:path'; import { escapePath as globEscape } from 'fast-glob'; -import { Sort, Path } from '@rushstack/node-core-library'; +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'; /** * This interface represents the raw pnpm-workspace.YAML file @@ -40,6 +41,36 @@ interface IPnpmWorkspaceYaml { * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) */ allowBuilds: Record | undefined; + /** + * 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 | undefined; + /** + * 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 | undefined; + /** + * 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 | undefined; + /** + * 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 | undefined; + /** + * 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 | 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) @@ -61,6 +92,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { private readonly _workspacePackages: Set; public catalogs: IPnpmWorkspaceYaml['catalogs']; public allowBuilds: IPnpmWorkspaceYaml['allowBuilds']; + public overrides: IPnpmWorkspaceYaml['overrides']; + public packageExtensions: IPnpmWorkspaceYaml['packageExtensions']; + public peerDependencyRules: IPnpmWorkspaceYaml['peerDependencyRules']; + public allowedDeprecatedVersions: IPnpmWorkspaceYaml['allowedDeprecatedVersions']; + public patchedDependencies: IPnpmWorkspaceYaml['patchedDependencies']; public minimumReleaseAge: IPnpmWorkspaceYaml['minimumReleaseAge']; public minimumReleaseAgeExclude: IPnpmWorkspaceYaml['minimumReleaseAgeExclude']; @@ -77,6 +113,21 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._workspacePackages = new Set(); } + /** + * Reads the `patchedDependencies` field from an existing `pnpm-workspace.yaml` file. + * @param workspaceYamlFilename - The path to the `pnpm-workspace.yaml` file + */ + public static async loadPatchedDependenciesAsync( + workspaceYamlFilename: string + ): Promise | undefined> { + const workspaceYamlContent: string = await FileSystem.readFileAsync(workspaceYamlFilename); + const yamlModule: typeof import('js-yaml') = await import('js-yaml'); + const workspaceYaml: IPnpmWorkspaceYaml | undefined = yamlModule.load(workspaceYamlContent) as + | IPnpmWorkspaceYaml + | undefined; + return workspaceYaml?.patchedDependencies; + } + public override addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file if (path.isAbsolute(packagePath)) { @@ -96,6 +147,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { _workspacePackages: workspacePackages, catalogs, allowBuilds, + overrides, + packageExtensions, + peerDependencyRules, + allowedDeprecatedVersions, + patchedDependencies, minimumReleaseAge, minimumReleaseAgeExclude } = this; @@ -105,6 +161,11 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { // An explicitly-set empty object is passed through as-is. catalogs, allowBuilds, + overrides, + packageExtensions, + peerDependencyRules, + allowedDeprecatedVersions, + patchedDependencies, 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 b4f772679d..0889b92a6f 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -207,6 +207,231 @@ describe(PnpmWorkspaceFile.name, () => { }); }); + describe('overrides functionality', () => { + it('generates workspace file with overrides', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.overrides = { + 'foo@1.0.0': '1.0.1', + bar: '^2.0.0' + }; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toContain('overrides:'); + expect(writtenContent).toMatchSnapshot(); + }); + + it('handles undefined overrides', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.overrides = undefined; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).not.toContain('overrides'); + }); + }); + + describe('packageExtensions functionality', () => { + it('generates workspace file with packageExtensions', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.packageExtensions = { + 'react@*': { + dependencies: { + foo: '1.0.0' + } + } + }; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toContain('packageExtensions:'); + expect(writtenContent).toMatchSnapshot(); + }); + + it('handles undefined packageExtensions', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.packageExtensions = undefined; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).not.toContain('packageExtensions'); + }); + }); + + describe('peerDependencyRules functionality', () => { + it('generates workspace file with peerDependencyRules', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.peerDependencyRules = { + ignoreMissing: ['baz'], + allowedVersions: { + react: '18' + } + }; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toContain('peerDependencyRules:'); + expect(writtenContent).toMatchSnapshot(); + }); + + it('handles undefined peerDependencyRules', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.peerDependencyRules = undefined; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).not.toContain('peerDependencyRules'); + }); + }); + + describe('allowedDeprecatedVersions functionality', () => { + it('generates workspace file with allowedDeprecatedVersions', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.allowedDeprecatedVersions = { + querystring: '*' + }; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toContain('allowedDeprecatedVersions:'); + expect(writtenContent).toMatchSnapshot(); + }); + + it('handles undefined allowedDeprecatedVersions', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.allowedDeprecatedVersions = undefined; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).not.toContain('allowedDeprecatedVersions'); + }); + }); + + describe('patchedDependencies functionality', () => { + it('generates workspace file with patchedDependencies', 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 }); + + expect(writtenContent).toContain('patchedDependencies:'); + expect(writtenContent).toMatchSnapshot(); + }); + + it('handles undefined patchedDependencies', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.patchedDependencies = undefined; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).not.toContain('patchedDependencies'); + }); + }); + + describe(PnpmWorkspaceFile.loadPatchedDependenciesAsync.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; + }); + }); + + 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 }); + + await expect(PnpmWorkspaceFile.loadPatchedDependenciesAsync(workspaceFilePath)).resolves.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 }); + + await expect( + PnpmWorkspaceFile.loadPatchedDependenciesAsync(workspaceFilePath) + ).resolves.toBeUndefined(); + }); + }); + + describe('combined pnpm 11 settings', () => { + it('generates workspace file with all relocated settings together', async () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(`${projectsDir}/app1`); + + workspaceFile.catalogs = { + default: { + react: '^18.0.0' + } + }; + workspaceFile.allowBuilds = { + esbuild: true + }; + workspaceFile.overrides = { + 'foo@1.0.0': '1.0.1' + }; + workspaceFile.packageExtensions = { + 'react@*': { + dependencies: { + foo: '1.0.0' + } + } + }; + workspaceFile.peerDependencyRules = { + allowedVersions: { + react: '18' + } + }; + workspaceFile.allowedDeprecatedVersions = { + querystring: '*' + }; + workspaceFile.patchedDependencies = { + 'lodash@4.17.21': 'patches/lodash@4.17.21.patch' + }; + + await workspaceFile.saveAsync(workspaceFilePath, { onlyIfChanged: true }); + + expect(writtenContent).toMatchSnapshot(); + }); + }); + describe('minimumReleaseAge functionality', () => { it('generates workspace file with minimumReleaseAge', async () => { const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); diff --git a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap index 39ea9278ef..0bfb0888ed 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 @@ -88,6 +96,30 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] = " `; +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 minimumReleaseAge functionality generates workspace file with minimumReleaseAge 1`] = ` "minimumReleaseAge: 20160 packages: @@ -112,3 +144,41 @@ packages: - projects/app1 " `; + +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 636522166e..97ba777f5b 100644 --- a/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts +++ b/libraries/rush-lib/src/logic/test/InstallHelpers.test.ts @@ -54,6 +54,7 @@ describe(InstallHelpers.name, () => { '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: { @@ -61,6 +62,18 @@ describe(InstallHelpers.name, () => { } } }, + 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 @@ -69,5 +82,28 @@ describe(InstallHelpers.name, () => { ); expect(packageJson).toMatchSnapshot(); }); + + it('omits the relocated pnpm settings for pnpm 11 (they belong in pnpm-workspace.yaml)', async () => { + const RUSH_JSON_FILENAME: string = `${__dirname}/pnpmConfigPnpm11/rush.json`; + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + await InstallHelpers.generateCommonPackageJsonAsync( + rushConfiguration, + rushConfiguration.defaultSubspace, + undefined, + terminal + ); + 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'); + }); }); }); 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 3138b8ae2e..c453f9719c 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 @@ -6,6 +6,9 @@ Object { "description": "Temporary file generated by the Rush tool", "name": "rush-common", "pnpm": Object { + "allowedDeprecatedVersions": Object { + "request": "*", + }, "neverBuiltDependencies": Array [ "fsevents", "level", @@ -27,6 +30,17 @@ Object { }, }, }, + "patchedDependencies": Object { + "lodash@4.17.21": "patches/lodash@4.17.21.patch", + }, + "peerDependencyRules": Object { + "allowedVersions": Object { + "react": "18", + }, + "ignoreMissing": Array [ + "@babel/core", + ], + }, "pnpmFutureFeature": true, }, "private": true, @@ -35,3 +49,5 @@ 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 []`; 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": [] +}