diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index a7016129ae6..ca5578e5133 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -883,8 +883,11 @@ ${gitLfsHookHandling} // On pnpm@8, disable the "dedupe-peer-dependents" feature when doing a filtered CI install so that filters take effect. args.push('--config.dedupe-peer-dependents=false'); } - } else if (experiments.usePnpmPreferFrozenLockfileForRushUpdate) { - // In workspaces, we want to avoid unnecessary lockfile churn + } else if (experiments.usePnpmPreferFrozenLockfileForRushUpdate && !onlyShrinkwrap) { + // In workspaces, we want to avoid unnecessary lockfile churn. + // Do NOT use --prefer-frozen-lockfile during the --lockfile-only phase: pnpm v10 + // omits the packages:/snapshots: sections when this combination is used, producing + // a broken lockfile that fails the subsequent --frozen-lockfile install phase. args.push('--prefer-frozen-lockfile'); } else { // Ensure that Rush's tarball dependencies get synchronized properly with the pnpm-lock.yaml file. diff --git a/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts b/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts index 21a2f460d3c..546f7575336 100644 --- a/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts +++ b/libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts @@ -129,4 +129,46 @@ describe('BaseInstallManager Test', () => { ); } }); + + it('usePnpmPreferFrozenLockfileForRushUpdate should not add --prefer-frozen-lockfile when onlyShrinkwrap is true', () => { + const rushJsonFile: string = path.resolve(__dirname, 'ignoreCompatibilityDb/rush3.json'); + const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + const purgeManager: typeof PurgeManager.prototype = new PurgeManager(rushConfiguration, rushGlobalFolder); + + // Enable the usePnpmPreferFrozenLockfileForRushUpdate experiment + Object.defineProperty(rushConfiguration.experimentsConfiguration, 'configuration', { + value: { usePnpmPreferFrozenLockfileForRushUpdate: true }, + writable: false, + configurable: true + }); + + const fakeBaseInstallManager: FakeBaseInstallManager = new FakeBaseInstallManager( + rushConfiguration, + rushGlobalFolder, + purgeManager, + { subspace: rushConfiguration.defaultSubspace } as IInstallManagerOptions + ); + + // When onlyShrinkwrap is true (Phase 1 of two-phase install), --prefer-frozen-lockfile must NOT be added. + // pnpm v10 omits packages:/snapshots: sections when --prefer-frozen-lockfile and --lockfile-only are + // combined, producing a broken lockfile that fails the subsequent --frozen-lockfile install phase. + const argsWithOnlyShrinkwrap: string[] = []; + fakeBaseInstallManager.pushConfigurationArgs( + argsWithOnlyShrinkwrap, + { onlyShrinkwrap: true, pnpmFilterArgumentValues: [] } as unknown as IInstallManagerOptions, + rushConfiguration.defaultSubspace + ); + expect(argsWithOnlyShrinkwrap).not.toContain('--prefer-frozen-lockfile'); + expect(argsWithOnlyShrinkwrap).toContain('--lockfile-only'); + + // When onlyShrinkwrap is false (single-phase or Phase 2), --prefer-frozen-lockfile should be added. + const argsWithoutOnlyShrinkwrap: string[] = []; + fakeBaseInstallManager.pushConfigurationArgs( + argsWithoutOnlyShrinkwrap, + { onlyShrinkwrap: false, pnpmFilterArgumentValues: [] } as unknown as IInstallManagerOptions, + rushConfiguration.defaultSubspace + ); + expect(argsWithoutOnlyShrinkwrap).toContain('--prefer-frozen-lockfile'); + expect(argsWithoutOnlyShrinkwrap).not.toContain('--lockfile-only'); + }); });