Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Use \"--prefer-frozen-lockfile\" instead of \"--frozen-lockfile\" for \"rush install\" with PNPM 12 or newer when a subspace has injected workspace dependencies, because native PNPM's frozen lockfile check rejects injected workspace dependencies outside the PNPM workspace root.",
"type": "patch"
}
],
"packageName": "@microsoft/rush",
"email": "223556219+Copilot@users.noreply.github.com"
}
17 changes: 16 additions & 1 deletion libraries/rush-lib/src/logic/base/BaseInstallManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,22 @@ ${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
) {
// Native PNPM's frozen-lockfile check reads injected workspace manifests without applying
// pnpmfile hooks, and rejects "workspace:" targets outside the PNPM workspace root. Every Rush
// project is outside common/temp, so that check always fails. Rush has already verified that
// the committed shrinkwrap satisfies all package.json files, so prefer the lockfile instead.
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 &&
Expand Down
64 changes: 58 additions & 6 deletions libraries/rush-lib/src/logic/test/BaseInstallManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,17 @@ describe('PNPM configuration arguments', () => {
jest.restoreAllMocks();
});

interface IConfigurationArgsScenario {
frozenLockfileForInstall?: boolean;
hasInjectedDependencies?: boolean;
}

function getConfigurationArgs(
pnpmVersion: string,
npmrcSettings: boolean,
pnpmOptions: Partial<Pick<PnpmOptionsConfiguration, 'autoInstallPeers' | 'resolutionMode'>> = {}
): { args: string[]; warnings: string } {
pnpmOptions: Partial<Pick<PnpmOptionsConfiguration, 'autoInstallPeers' | 'resolutionMode'>> = {},
{ frozenLockfileForInstall = false, hasInjectedDependencies = false }: IConfigurationArgsScenario = {}
): { args: string[]; output: string; warnings: string } {
jest.spyOn(npmrcUtilities, 'isVariableSetInNpmrcFile').mockReturnValue(npmrcSettings);
const terminalProvider: StringBufferTerminalProvider = new StringBufferTerminalProvider();
const manager: FakeBaseInstallManager = Object.create(FakeBaseInstallManager.prototype);
Expand All @@ -162,19 +168,65 @@ 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() };
return { args, output: terminalProvider.getOutput(), warnings: terminalProvider.getWarningOutput() };
}

it.each([
['10.27.0', true],
['12.10.1', false]
])(
'uses a frozen lockfile for install with PNPM %s when injected dependencies=%s',
(pnpmVersion: string, hasInjectedDependencies: boolean) => {
const { args, output } = getConfigurationArgs(
pnpmVersion,
false,
{},
{
frozenLockfileForInstall: true,
hasInjectedDependencies
}
);

expect(args).toContain('--frozen-lockfile');
expect(args).not.toContain('--no-frozen-lockfile');
expect(args).not.toContain('--prefer-frozen-lockfile');
expect(output).not.toContain('injected workspace dependencies');
}
);

it('prefers the lockfile for install with native PNPM and injected dependencies', () => {
const { args, output, warnings } = getConfigurationArgs(
'12.10.1',
false,
{},
{
frozenLockfileForInstall: true,
hasInjectedDependencies: true
}
);

expect(args).toContain('--no-frozen-lockfile');
expect(args).toContain('--prefer-frozen-lockfile');
expect(args).not.toContain('--frozen-lockfile');
expect(output).toContain('cannot verify injected workspace dependencies with "--frozen-lockfile"');
expect(warnings).toBe('');
});

it.each(['8.14.0', '12.11.2'])(
'uses Rush defaults when settings are absent with PNPM %s',
(pnpmVersion: string) => {
Expand Down
Loading