From 835709b4ecd511120bd982b28a7e29ba9915b029 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:21:37 +0000 Subject: [PATCH 1/3] Initial plan From 440a1b3a5929a8fb9fa960d022e87ba2cc5e9cb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:26:09 +0000 Subject: [PATCH 2/3] Add pnpm.storePath to RushUserConfiguration and integrate with PnpmOptionsConfiguration Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com> --- .../rush-lib/src/api/RushConfiguration.ts | 15 ++++++++-- .../rush-lib/src/api/RushUserConfiguration.ts | 28 +++++++++++++++++++ libraries/rush-lib/src/api/Subspace.ts | 3 +- .../logic/pnpm/PnpmOptionsConfiguration.ts | 15 ++++++---- .../schemas/rush-user-settings.schema.json | 12 ++++++++ 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/libraries/rush-lib/src/api/RushConfiguration.ts b/libraries/rush-lib/src/api/RushConfiguration.ts index c1398ea0622..e306d22c183 100644 --- a/libraries/rush-lib/src/api/RushConfiguration.ts +++ b/libraries/rush-lib/src/api/RushConfiguration.ts @@ -46,6 +46,7 @@ import type { PackageManagerOptionsConfigurationBase } from '../logic/base/BaseP import { CustomTipsConfiguration } from './CustomTipsConfiguration'; import { SubspacesConfiguration } from './SubspacesConfiguration'; import { Subspace } from './Subspace'; +import { RushUserConfiguration } from './RushUserConfiguration'; const MINIMUM_SUPPORTED_RUSH_JSON_VERSION: string = '0.0.0'; const DEFAULT_BRANCH: string = 'main'; @@ -247,6 +248,11 @@ export class RushConfiguration { private readonly _subspacesByName: Map; private readonly _subspaces: Subspace[] = []; + /** + * @internal + */ + public readonly _rushUserConfiguration: RushUserConfiguration; + /** * The name of the package manager being used to install dependencies */ @@ -677,12 +683,16 @@ export class RushConfiguration { ); this._rushPluginsConfiguration = new RushPluginsConfiguration(rushPluginsConfigFilename); + // Load user configuration for per-user settings like pnpm store path + this._rushUserConfiguration = RushUserConfiguration.initialize(); + this.npmOptions = new NpmOptionsConfiguration(rushConfigurationJson.npmOptions || {}); this.yarnOptions = new YarnOptionsConfiguration(rushConfigurationJson.yarnOptions || {}); try { this.pnpmOptions = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( `${this.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}`, - this.commonTempFolder + this.commonTempFolder, + this._rushUserConfiguration ); if (rushConfigurationJson.pnpmOptions) { throw new Error( @@ -694,7 +704,8 @@ export class RushConfiguration { if (FileSystem.isNotExistError(error as Error)) { this.pnpmOptions = PnpmOptionsConfiguration.loadFromJsonObject( rushConfigurationJson.pnpmOptions || {}, - this.commonTempFolder + this.commonTempFolder, + this._rushUserConfiguration ); } else { throw error; diff --git a/libraries/rush-lib/src/api/RushUserConfiguration.ts b/libraries/rush-lib/src/api/RushUserConfiguration.ts index cdf767363bb..a81697583f3 100644 --- a/libraries/rush-lib/src/api/RushUserConfiguration.ts +++ b/libraries/rush-lib/src/api/RushUserConfiguration.ts @@ -10,6 +10,9 @@ import schemaJson from '../schemas/rush-user-settings.schema.json'; interface IRushUserSettingsJson { buildCacheFolder?: string; + pnpm?: { + storePath?: string; + }; } /** @@ -25,11 +28,18 @@ export class RushUserConfiguration { */ public readonly buildCacheFolder: string | undefined; + /** + * If provided, specifies the path for the PNPM store directory. + * Can be overridden by the RUSH_PNPM_STORE_PATH environment variable. + */ + public readonly pnpmStorePath: string | undefined; + private constructor(rushUserConfigurationJson: IRushUserSettingsJson | undefined) { this.buildCacheFolder = rushUserConfigurationJson?.buildCacheFolder; if (this.buildCacheFolder && !path.isAbsolute(this.buildCacheFolder)) { throw new Error('buildCacheFolder must be an absolute path'); } + this.pnpmStorePath = rushUserConfigurationJson?.pnpm?.storePath; } public static async initializeAsync(): Promise { @@ -50,6 +60,24 @@ export class RushUserConfiguration { return new RushUserConfiguration(rushUserSettingsJson); } + public static initialize(): RushUserConfiguration { + const rushUserFolderPath: string = RushUserConfiguration.getRushUserFolderPath(); + const rushUserSettingsFilePath: string = path.join(rushUserFolderPath, 'settings.json'); + let rushUserSettingsJson: IRushUserSettingsJson | undefined; + try { + rushUserSettingsJson = JsonFile.loadAndValidate( + rushUserSettingsFilePath, + RushUserConfiguration._schema + ); + } catch (e) { + if (!FileSystem.isNotExistError(e as Error)) { + throw e; + } + } + + return new RushUserConfiguration(rushUserSettingsJson); + } + public static getRushUserFolderPath(): string { const homeFolderPath: string = User.getHomeFolder(); return `${homeFolderPath}/${RushConstants.rushUserConfigurationFolderName}`; diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 624b1a33bd6..57a21696a14 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -80,7 +80,8 @@ export class Subspace { try { this._cachedPnpmOptions = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( this.getPnpmConfigFilePath(), - subspaceTempFolder + subspaceTempFolder, + this._rushConfiguration._rushUserConfiguration ); this._cachedPnpmOptionsInitialized = true; } catch (e) { diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 026ecd29339..f6127d466f0 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -10,6 +10,7 @@ import { PackageManagerOptionsConfigurationBase } from '../base/BasePackageManagerOptionsConfiguration'; import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; +import type { RushUserConfiguration } from '../../api/RushUserConfiguration'; import schemaJson from '../../schemas/pnpm-config.schema.json'; /** @@ -466,13 +467,15 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration return this._globalPatchedDependencies; } - private constructor(json: IPnpmOptionsJson, commonTempFolder: string, jsonFilename?: string) { + private constructor(json: IPnpmOptionsJson, commonTempFolder: string, jsonFilename?: string, rushUserConfiguration?: RushUserConfiguration) { super(json); this._json = json; this.jsonFilename = jsonFilename; this.pnpmStore = json.pnpmStore || 'local'; if (EnvironmentConfiguration.pnpmStorePathOverride) { this.pnpmStorePath = EnvironmentConfiguration.pnpmStorePathOverride; + } else if (rushUserConfiguration?.pnpmStorePath) { + this.pnpmStorePath = rushUserConfiguration.pnpmStorePath; } else if (this.pnpmStore === 'global') { this.pnpmStorePath = ''; } else { @@ -504,7 +507,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration /** @internal */ public static loadFromJsonFileOrThrow( jsonFilePath: string, - commonTempFolder: string + commonTempFolder: string, + rushUserConfiguration?: RushUserConfiguration ): PnpmOptionsConfiguration { // TODO: plumb through the terminal const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); @@ -518,15 +522,16 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration jsonFilePath ); pnpmConfigJson.$schema = pnpmOptionsConfigFile.getSchemaPropertyOriginalValue(pnpmConfigJson); - return new PnpmOptionsConfiguration(pnpmConfigJson || {}, commonTempFolder, jsonFilePath); + return new PnpmOptionsConfiguration(pnpmConfigJson || {}, commonTempFolder, jsonFilePath, rushUserConfiguration); } /** @internal */ public static loadFromJsonObject( json: IPnpmOptionsJson, - commonTempFolder: string + commonTempFolder: string, + rushUserConfiguration?: RushUserConfiguration ): PnpmOptionsConfiguration { - return new PnpmOptionsConfiguration(json, commonTempFolder); + return new PnpmOptionsConfiguration(json, commonTempFolder, undefined, rushUserConfiguration); } /** diff --git a/libraries/rush-lib/src/schemas/rush-user-settings.schema.json b/libraries/rush-lib/src/schemas/rush-user-settings.schema.json index dde6f7e06f3..37f64e59be0 100644 --- a/libraries/rush-lib/src/schemas/rush-user-settings.schema.json +++ b/libraries/rush-lib/src/schemas/rush-user-settings.schema.json @@ -13,6 +13,18 @@ "buildCacheFolder": { "type": "string", "description": "If provided, store build cache in the specified folder. Must be an absolute path." + }, + + "pnpm": { + "type": "object", + "description": "PNPM-specific user configuration options.", + "properties": { + "storePath": { + "type": "string", + "description": "If provided, specifies the path for the PNPM store directory. Can be overridden by the RUSH_PNPM_STORE_PATH environment variable." + } + }, + "additionalProperties": false } }, "additionalProperties": false From 2de45db74c4bb347eb21bc1bea82d68929a35560 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 01:38:21 +0000 Subject: [PATCH 3/3] Add tests for pnpm.storePath functionality in RushUserConfiguration Co-authored-by: dmichon-msft <26827560+dmichon-msft@users.noreply.github.com> --- common/reviews/api/rush-lib.api.md | 9 ++- .../test/PnpmOptionsConfiguration.test.ts | 67 +++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 1f8def84f93..b5c99e3cc7d 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1171,9 +1171,9 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration // (undocumented) readonly jsonFilename: string | undefined; // @internal (undocumented) - static loadFromJsonFileOrThrow(jsonFilePath: string, commonTempFolder: string): PnpmOptionsConfiguration; + static loadFromJsonFileOrThrow(jsonFilePath: string, commonTempFolder: string, rushUserConfiguration?: RushUserConfiguration): PnpmOptionsConfiguration; // @internal (undocumented) - static loadFromJsonObject(json: _IPnpmOptionsJson, commonTempFolder: string): PnpmOptionsConfiguration; + static loadFromJsonObject(json: _IPnpmOptionsJson, commonTempFolder: string, rushUserConfiguration?: RushUserConfiguration): PnpmOptionsConfiguration; readonly minimumReleaseAge: number | undefined; readonly minimumReleaseAgeExclude: string[] | undefined; readonly pnpmLockfilePolicies: IPnpmLockfilePolicies | undefined; @@ -1338,6 +1338,8 @@ export class RushConfiguration { // // @internal (undocumented) readonly _rushPluginsConfiguration: RushPluginsConfiguration; + // @internal (undocumented) + readonly _rushUserConfiguration: RushUserConfiguration; readonly shrinkwrapFilename: string; get shrinkwrapFilePhrase(): string; // @beta @@ -1558,7 +1560,10 @@ export class RushUserConfiguration { // (undocumented) static getRushUserFolderPath(): string; // (undocumented) + static initialize(): RushUserConfiguration; + // (undocumented) static initializeAsync(): Promise; + readonly pnpmStorePath: string | undefined; } // @public diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 369054709ad..357b47d3259 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -4,6 +4,8 @@ import * as path from 'node:path'; import { PnpmOptionsConfiguration } from '../PnpmOptionsConfiguration'; import { TestUtilities } from '@rushstack/heft-config-file'; +import { EnvironmentConfiguration } from '../../../api/EnvironmentConfiguration'; +import type { RushUserConfiguration } from '../../../api/RushUserConfiguration'; const fakeCommonTempFolder: string = path.join(__dirname, 'common', 'temp'); @@ -122,4 +124,69 @@ describe(PnpmOptionsConfiguration.name, () => { } }); }); + + it('uses default store path when no user configuration is provided', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonObject( + {}, + fakeCommonTempFolder + ); + + expect(pnpmConfiguration.pnpmStorePath).toEqual(`${fakeCommonTempFolder}/pnpm-store`); + }); + + it('uses user configuration storePath when provided', () => { + const mockUserConfig: Partial = { + buildCacheFolder: undefined, + pnpmStorePath: '/custom/pnpm/store' + }; + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonObject( + {}, + fakeCommonTempFolder, + mockUserConfig as RushUserConfiguration + ); + + expect(pnpmConfiguration.pnpmStorePath).toEqual('/custom/pnpm/store'); + }); + + it('environment variable takes precedence over user configuration', () => { + // Save original value + const originalEnvValue = process.env.RUSH_PNPM_STORE_PATH; + + try { + // Set environment variable with a path that will be normalized + // Use an absolute path like the temp folder + const envStorePath: string = path.join(fakeCommonTempFolder, 'env-pnpm-store'); + process.env.RUSH_PNPM_STORE_PATH = envStorePath; + + // Re-validate environment configuration to pick up the change + EnvironmentConfiguration.reset(); + EnvironmentConfiguration.validate(); + + const mockUserConfig: Partial = { + buildCacheFolder: undefined, + pnpmStorePath: '/custom/pnpm/store' + }; + + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonObject( + {}, + fakeCommonTempFolder, + mockUserConfig as RushUserConfiguration + ); + + // The environment variable value should be used (after normalization) + expect(pnpmConfiguration.pnpmStorePath).toContain('env-pnpm-store'); + } finally { + // Restore original value + if (originalEnvValue !== undefined) { + process.env.RUSH_PNPM_STORE_PATH = originalEnvValue; + } else { + delete process.env.RUSH_PNPM_STORE_PATH; + } + + // Re-validate to restore original state + EnvironmentConfiguration.reset(); + EnvironmentConfiguration.validate(); + } + }); });