Skip to content
Open
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,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Fix an issue where \"rush-pnpm patch-commit\" rewrote the pre-existing \"globalPatchedDependencies\" entries in pnpm-config.json using absolute paths when running with pnpm >= 9.",
"type": "patch"
}
],
"packageName": "@microsoft/rush"
}
23 changes: 22 additions & 1 deletion libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { JsonFile, type JsonObject } from '@rushstack/node-core-library';
import * as path from 'node:path';

import { JsonFile, type JsonObject, Path } from '@rushstack/node-core-library';
import { NonProjectConfigurationFile } from '@rushstack/heft-config-file';
import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal';

Expand Down Expand Up @@ -211,6 +213,7 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
*/
export class PnpmOptionsConfiguration extends PackageManagerOptionsConfigurationBase {
private readonly _json: JsonObject;
private readonly _commonTempFolder: string;
private _globalPatchedDependencies: Record<string, string> | undefined;

/**
Expand Down Expand Up @@ -556,6 +559,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
private constructor(json: IPnpmOptionsJson, commonTempFolder: string, jsonFilename?: string) {
super(json);
this._json = json;
this._commonTempFolder = commonTempFolder;
this.jsonFilename = jsonFilename;
this.pnpmStore = json.pnpmStore || 'local';
if (EnvironmentConfiguration.pnpmStorePathOverride) {
Expand Down Expand Up @@ -636,8 +640,25 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration

/**
* Updates patchedDependencies field of the PNPM options in the common/config/rush/pnpm-config.json file.
*
* @remarks
* When running "pnpm patch-commit"/"pnpm patch-remove" (pnpm 9 and newer), pnpm rewrites the
* pre-existing entries using absolute paths pointing into the common/temp folder. Normalize any such
* value back to a path relative to the common/temp folder (e.g. "patches/example\@1.0.0.patch") so the location
* of the local checkout does not leak into pnpm-config.json.
*/
public updateGlobalPatchedDependencies(patchedDependencies: Record<string, string> | undefined): void {
if (patchedDependencies) {
const normalized: Record<string, string> = {};
for (const [dependency, patchPath] of Object.entries(patchedDependencies)) {
normalized[dependency] =
path.isAbsolute(patchPath) && Path.isUnder(patchPath, this._commonTempFolder)
? Path.convertToSlashes(path.relative(this._commonTempFolder, patchPath))
: patchPath;
}
patchedDependencies = normalized;
}

this._globalPatchedDependencies = patchedDependencies;
this._json.globalPatchedDependencies = patchedDependencies;
if (this.jsonFilename) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,57 @@ describe(PnpmOptionsConfiguration.name, () => {
}
});
});

describe('updateGlobalPatchedDependencies', () => {
function update(
patchedDependencies: Record<string, string> | undefined
): Record<string, string> | undefined {
// No jsonFilename, so updateGlobalPatchedDependencies won't try to write to disk
const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonObject(
{},
fakeCommonTempFolder
);
pnpmConfiguration.updateGlobalPatchedDependencies(patchedDependencies);
return pnpmConfiguration.globalPatchedDependencies;
}

it('converts absolute patch paths under the common temp folder back to relative paths', () => {
// pnpm >= 9 "patch-commit"/"patch-remove" rewrite pre-existing "patchedDependencies" entries
// using absolute paths pointing into the common/temp folder
expect(
update({
'example@1.0.0': path.join(fakeCommonTempFolder, 'patches', 'example@1.0.0.patch'),
'@scope/example2@2.0.0': path.join(fakeCommonTempFolder, 'patches', '@scope__example2@2.0.0.patch')
})
).toEqual({
'example@1.0.0': 'patches/example@1.0.0.patch',
'@scope/example2@2.0.0': 'patches/@scope__example2@2.0.0.patch'
});
});

it('leaves relative patch paths unchanged', () => {
expect(
update({
'example@1.0.0': 'patches/example@1.0.0.patch'
})
).toEqual({
'example@1.0.0': 'patches/example@1.0.0.patch'
});
});

it('leaves absolute patch paths outside the common temp folder unchanged', () => {
const outsidePath: string = path.join(path.sep, 'somewhere', 'else', 'example@1.0.0.patch');
expect(
update({
'example@1.0.0': outsidePath
})
).toEqual({
'example@1.0.0': outsidePath
});
});

it('passes through undefined', () => {
expect(update(undefined)).toBeUndefined();
});
});
});