From 4c77fcbe1ff56e38b299696239c8187d7665adb3 Mon Sep 17 00:00:00 2001 From: Bharat Middha <5100938+bmiddha@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:37:24 -0700 Subject: [PATCH] [rush] Fix "rush install" with pnpm 12 and injected workspace dependencies pnpm 12's frozen-lockfile check rejects injected "workspace:" dependencies outside the pnpm workspace root, which every Rush project is. For those subspaces, "rush install" now uses --prefer-frozen-lockfile; Rush has already validated the shrinkwrap against each package.json. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...cted-frozen-lockfile_2026-10-14-18-22.json | 11 ++++++ .../src/logic/base/BaseInstallManager.ts | 15 +++++++- .../src/logic/test/BaseInstallManager.test.ts | 37 +++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 common/changes/@microsoft/rush/pnpm12-injected-frozen-lockfile_2026-10-14-18-22.json diff --git a/common/changes/@microsoft/rush/pnpm12-injected-frozen-lockfile_2026-10-14-18-22.json b/common/changes/@microsoft/rush/pnpm12-injected-frozen-lockfile_2026-10-14-18-22.json new file mode 100644 index 00000000000..0fc6111349e --- /dev/null +++ b/common/changes/@microsoft/rush/pnpm12-injected-frozen-lockfile_2026-10-14-18-22.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix \"rush install\" failing with ERR_PNPM_OUTDATED_LOCKFILE on PNPM 12 for subspaces with injected workspace dependencies, which now install with \"--prefer-frozen-lockfile\".", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index 9ff25e2aa62..a636b4b8234 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -924,7 +924,20 @@ ${gitLfsHookHandling} const { configuration: experiments } = this.rushConfiguration.experimentsConfiguration; if (experiments.usePnpmFrozenLockfileForRushInstall && !allowShrinkwrapUpdates) { - args.push('--frozen-lockfile'); + if ( + semver.gte(this.rushConfiguration.packageManagerToolVersion, '12.0.0') && + subspace.getPackageJsonInjectedDependenciesHash(options.variant) !== undefined + ) { + // PNPM 12's frozen check rejects injected "workspace:" dependencies outside the workspace root, + // which is every Rush project. Rush has already checked the shrinkwrap against each package.json. + this._terminal.writeLine( + `PNPM ${this.rushConfiguration.packageManagerToolVersion} cannot verify injected workspace ` + + 'dependencies with "--frozen-lockfile"; using "--prefer-frozen-lockfile" instead.' + ); + args.push('--no-frozen-lockfile', '--prefer-frozen-lockfile'); + } else { + args.push('--frozen-lockfile'); + } if ( pnpmFilterArgumentValues.length > 0 && diff --git a/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts b/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts index a42f25a86ad..d14fe73b780 100644 --- a/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts +++ b/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts @@ -148,10 +148,16 @@ describe('PNPM configuration arguments', () => { jest.restoreAllMocks(); }); + interface IConfigurationArgsScenario { + frozenLockfileForInstall?: boolean; + hasInjectedDependencies?: boolean; + } + function getConfigurationArgs( pnpmVersion: string, npmrcSettings: boolean, - pnpmOptions: Partial> = {} + pnpmOptions: Partial> = {}, + { frozenLockfileForInstall = false, hasInjectedDependencies = false }: IConfigurationArgsScenario = {} ): { args: string[]; warnings: string } { jest.spyOn(npmrcUtilities, 'isVariableSetInNpmrcFile').mockReturnValue(npmrcSettings); const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider(); @@ -162,19 +168,42 @@ describe('PNPM configuration arguments', () => { isPnpm: true, packageManagerToolVersion: pnpmVersion, pnpmOptions: { pnpmStore: 'global', pnpmStorePath: '/repo/common/temp/pnpm-store', ...pnpmOptions }, - experimentsConfiguration: { configuration: {} } + experimentsConfiguration: { + configuration: { usePnpmFrozenLockfileForRushInstall: frozenLockfileForInstall } + } }, _terminal: new Terminal(terminalProvider) }); const args: string[] = []; manager.pushConfigurationArgs( args, - { pnpmFilterArgumentValues: [] } as unknown as IInstallManagerOptions, - { getSubspaceConfigFolderPath: () => '/repo/common/config/rush' } as Subspace + { pnpmFilterArgumentValues: [], allowShrinkwrapUpdates: false } as unknown as IInstallManagerOptions, + { + getSubspaceConfigFolderPath: () => '/repo/common/config/rush', + getPackageJsonInjectedDependenciesHash: () => (hasInjectedDependencies ? 'hash' : undefined) + } as unknown as Subspace ); return { args, warnings: terminalProvider.getWarningOutput() }; } + it.each([ + ['10.27.0', true, ['--frozen-lockfile']], + ['12.10.1', false, ['--frozen-lockfile']], + ['12.10.1', true, ['--no-frozen-lockfile', '--prefer-frozen-lockfile']] + ])( + 'passes lockfile flags for install with PNPM %s and injected dependencies=%s', + (pnpmVersion: string, hasInjectedDependencies: boolean, expectedFlags: string[]) => { + const { args } = getConfigurationArgs( + pnpmVersion, + false, + {}, + { frozenLockfileForInstall: true, hasInjectedDependencies } + ); + + expect(args.filter((arg) => arg.includes('frozen-lockfile'))).toEqual(expectedFlags); + } + ); + it.each(['8.14.0', '12.11.2'])( 'uses Rush defaults when settings are absent with PNPM %s', (pnpmVersion: string) => {