diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 454ac3daa07..866e2bcee43 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -248,6 +248,7 @@ export class EnvironmentConfiguration { static parseBooleanEnvironmentVariable(name: string, value: string | undefined): boolean | undefined; static get pnpmStorePathOverride(): string | undefined; static get pnpmVerifyStoreIntegrity(): boolean | undefined; + static get pnpmVirtualStorePathOverride(): string | undefined; static reset(): void; static get rushGlobalFolderOverride(): string | undefined; static get rushTempFolderOverride(): string | undefined; @@ -265,6 +266,7 @@ export const EnvironmentVariableNames: { readonly RUSH_PARALLELISM: "RUSH_PARALLELISM"; readonly RUSH_ABSOLUTE_SYMLINKS: "RUSH_ABSOLUTE_SYMLINKS"; readonly RUSH_PNPM_STORE_PATH: "RUSH_PNPM_STORE_PATH"; + readonly RUSH_PNPM_VIRTUAL_STORE_PATH: "RUSH_PNPM_VIRTUAL_STORE_PATH"; readonly RUSH_PNPM_VERIFY_STORE_INTEGRITY: "RUSH_PNPM_VERIFY_STORE_INTEGRITY"; readonly RUSH_DEPLOY_TARGET_FOLDER: "RUSH_DEPLOY_TARGET_FOLDER"; readonly RUSH_GLOBAL_FOLDER: "RUSH_GLOBAL_FOLDER"; @@ -1103,6 +1105,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly pnpmLockfilePolicies: IPnpmLockfilePolicies | undefined; readonly pnpmStore: PnpmStoreLocation; readonly pnpmStorePath: string; + readonly pnpmVirtualStorePath: string; readonly preventManualShrinkwrapChanges: boolean; readonly resolutionMode: PnpmResolutionMode | undefined; readonly strictPeerDependencies: boolean; diff --git a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts index da883ddff60..12e8936f285 100644 --- a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts +++ b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts @@ -25,7 +25,8 @@ export const EnvironmentVariableNames = { * The default value is "common/temp" under the repository root. * * @remarks This environment variable is not compatible with workspace installs. If attempting - * to move the PNPM store path, see the `RUSH_PNPM_STORE_PATH` environment variable. + * to move the PNPM store or virtual store paths, see the `RUSH_PNPM_STORE_PATH` and + * `RUSH_PNPM_VIRTUAL_STORE_PATH` environment variables, respectively. */ RUSH_TEMP_FOLDER: 'RUSH_TEMP_FOLDER', @@ -80,6 +81,15 @@ export const EnvironmentVariableNames = { */ RUSH_PNPM_STORE_PATH: 'RUSH_PNPM_STORE_PATH', + /** + * When using PNPM as the package manager, this variable can be used to configure the path that + * PNPM will use as the virtual store directory. + * + * If a relative path is used, then the virtual store path will be resolved relative to the process's + * current working directory. An absolute path is recommended. + */ + RUSH_PNPM_VIRTUAL_STORE_PATH: 'RUSH_PNPM_VIRTUAL_STORE_PATH', + /** * When using PNPM as the package manager, this variable can be used to control whether or not PNPM * validates the integrity of the PNPM store during installation. The value of this environment variable must be @@ -240,6 +250,8 @@ export class EnvironmentConfiguration { private static _pnpmStorePathOverride: string | undefined; + private static _pnpmVirtualStorePathOverride: string | undefined; + private static _pnpmVerifyStoreIntegrity: boolean | undefined; private static _rushGlobalFolderOverride: string | undefined; @@ -300,7 +312,7 @@ export class EnvironmentConfiguration { } /** - * An override for the PNPM store path, if `pnpmStore` configuration is set to 'path' + * An override for the PNPM store path. * See {@link EnvironmentVariableNames.RUSH_PNPM_STORE_PATH} */ public static get pnpmStorePathOverride(): string | undefined { @@ -308,6 +320,15 @@ export class EnvironmentConfiguration { return EnvironmentConfiguration._pnpmStorePathOverride; } + /** + * An override for the PNPM virtual store path. + * See {@link EnvironmentVariableNames.RUSH_PNPM_VIRTUAL_STORE_PATH} + */ + public static get pnpmVirtualStorePathOverride(): string | undefined { + EnvironmentConfiguration._ensureValidated(); + return EnvironmentConfiguration._pnpmVirtualStorePathOverride; + } + /** * If specified, enables or disables integrity verification of the pnpm store during install. * See {@link EnvironmentVariableNames.RUSH_PNPM_VERIFY_STORE_INTEGRITY} @@ -476,6 +497,14 @@ export class EnvironmentConfiguration { break; } + case EnvironmentVariableNames.RUSH_PNPM_VIRTUAL_STORE_PATH: { + EnvironmentConfiguration._pnpmVirtualStorePathOverride = + value && !options.doNotNormalizePaths + ? EnvironmentConfiguration._normalizeDeepestParentFolderPath(value) || value + : value; + break; + } + case EnvironmentVariableNames.RUSH_PNPM_VERIFY_STORE_INTEGRITY: { EnvironmentConfiguration._pnpmVerifyStoreIntegrity = value === '1' ? true : value === '0' ? false : undefined; diff --git a/libraries/rush-lib/src/api/LastInstallFlag.ts b/libraries/rush-lib/src/api/LastInstallFlag.ts index 2d7a2a6db01..98f9fab9a39 100644 --- a/libraries/rush-lib/src/api/LastInstallFlag.ts +++ b/libraries/rush-lib/src/api/LastInstallFlag.ts @@ -40,6 +40,10 @@ export interface ILastInstallFlagJson { * Same with pnpmOptions.pnpmStorePath in rush.json */ storePath?: string; + /** + * Same with pnpmOptions.pnpmVirtualStorePath in rush.json + */ + virtualStorePath?: string; /** * An experimental flag used by cleanInstallAfterNpmrcChanges */ @@ -205,6 +209,7 @@ export function getCommonTempFlag( if (currentState.packageManager === 'pnpm' && rushConfiguration.pnpmOptions) { currentState.storePath = rushConfiguration.pnpmOptions.pnpmStorePath; + currentState.virtualStorePath = rushConfiguration.pnpmOptions.pnpmVirtualStorePath; if (rushConfiguration.pnpmOptions.useWorkspaces) { currentState.workspaces = rushConfiguration.pnpmOptions.useWorkspaces; } diff --git a/libraries/rush-lib/src/api/test/EnvironmentConfiguration.test.ts b/libraries/rush-lib/src/api/test/EnvironmentConfiguration.test.ts index 83ebc055b5a..7fa0d6bc4cd 100644 --- a/libraries/rush-lib/src/api/test/EnvironmentConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/EnvironmentConfiguration.test.ts @@ -108,4 +108,32 @@ describe(EnvironmentConfiguration.name, () => { expect(EnvironmentConfiguration.pnpmStorePathOverride).toEqual(expectedValue); }); }); + + describe('pnpmVirtualStorePathOverride', () => { + const ENV_VAR: string = 'RUSH_PNPM_VIRTUAL_STORE_PATH'; + + it('returns undefined for unset environment variable', () => { + EnvironmentConfiguration.validate(); + + expect(EnvironmentConfiguration.pnpmVirtualStorePathOverride).not.toBeDefined(); + }); + + it('returns the expected path from environment variable without normalization', () => { + const expectedValue: string = '/var/temp'; + process.env[ENV_VAR] = expectedValue; + EnvironmentConfiguration.validate({ doNotNormalizePaths: true }); + + expect(EnvironmentConfiguration.pnpmVirtualStorePathOverride).toEqual(expectedValue); + }); + + it('returns expected path from environment variable with normalization', () => { + const expectedValue: string = path.resolve(process.cwd(), 'temp'); + const envVar: string = './temp'; + process.env[ENV_VAR] = envVar; + + EnvironmentConfiguration.validate(); + + expect(EnvironmentConfiguration.pnpmVirtualStorePathOverride).toEqual(expectedValue); + }); + }); }); diff --git a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts index f5edac12662..6d752dcfd6d 100644 --- a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts @@ -63,6 +63,11 @@ describe(RushConfiguration.name, () => { rushConfiguration.pnpmOptions.pnpmStorePath, './repo/common/temp/pnpm-store' ); + assertPathProperty( + 'pnpmVirtualStorePath', + rushConfiguration.pnpmOptions.pnpmVirtualStorePath, + './repo/common/temp/.pnpm' + ); assertPathProperty( 'packageManagerToolFilename', rushConfiguration.packageManagerToolFilename, @@ -136,6 +141,11 @@ describe(RushConfiguration.name, () => { rushConfiguration.pnpmOptions.pnpmStorePath, './repo/common/temp/pnpm-store' ); + assertPathProperty( + 'pnpmVirtualStorePath', + rushConfiguration.pnpmOptions.pnpmVirtualStorePath, + './repo/common/temp/.pnpm' + ); assertPathProperty( 'packageManagerToolFilename', rushConfiguration.packageManagerToolFilename, @@ -212,6 +222,11 @@ describe(RushConfiguration.name, () => { rushConfiguration.pnpmOptions.pnpmStorePath, path.join(expectedValue, 'pnpm-store') ); + assertPathProperty( + 'pnpmVirtualStorePath', + rushConfiguration.pnpmOptions.pnpmVirtualStorePath, + path.join(expectedValue, '.pnpm') + ); assertPathProperty( 'packageManagerToolFilename', rushConfiguration.packageManagerToolFilename, @@ -227,15 +242,18 @@ describe(RushConfiguration.name, () => { describe('PNPM Store Paths', () => { afterEach(() => { EnvironmentConfiguration['_pnpmStorePathOverride'] = undefined; + EnvironmentConfiguration['_pnpmVirtualStorePathOverride'] = undefined; }); const PNPM_STORE_PATH_ENV: string = 'RUSH_PNPM_STORE_PATH'; + const PNPM_VIRTUAL_STORE_PATH_ENV: string = 'RUSH_PNPM_VIRTUAL_STORE_PATH'; describe('Loading repo/rush-pnpm-local.json', () => { const RUSH_JSON_FILENAME: string = path.resolve(__dirname, 'repo', 'rush-pnpm-local.json'); it(`loads the correct path when pnpmStore = "local"`, () => { const EXPECT_STORE_PATH: string = path.resolve(__dirname, 'repo', 'common', 'temp', 'pnpm-store'); + const EXPECT_VIRTUAL_STORE_PATH: string = path.resolve(__dirname, 'repo', 'common', 'temp', '.pnpm'); const rushConfiguration: RushConfiguration = RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); @@ -245,9 +263,13 @@ describe(RushConfiguration.name, () => { Path.convertToSlashes(EXPECT_STORE_PATH) ); expect(path.isAbsolute(rushConfiguration.pnpmOptions.pnpmStorePath)).toEqual(true); + expect(Path.convertToSlashes(rushConfiguration.pnpmOptions.pnpmVirtualStorePath)).toEqual( + Path.convertToSlashes(EXPECT_VIRTUAL_STORE_PATH) + ); + expect(path.isAbsolute(rushConfiguration.pnpmOptions.pnpmVirtualStorePath)).toEqual(true); }); - it('loads the correct path when environment variable is defined', () => { + it('loads the correct store path when environment variable is defined', () => { const EXPECT_STORE_PATH: string = path.resolve('/var/temp'); process.env[PNPM_STORE_PATH_ENV] = EXPECT_STORE_PATH; @@ -259,6 +281,19 @@ describe(RushConfiguration.name, () => { expect(rushConfiguration.pnpmOptions.pnpmStorePath).toEqual(EXPECT_STORE_PATH); expect(path.isAbsolute(rushConfiguration.pnpmOptions.pnpmStorePath)).toEqual(true); }); + + it('loads the correct virtual store path when environment variable is defined', () => { + const EXPECT_VIRTUAL_STORE_PATH: string = path.resolve('/var/temp'); + process.env[PNPM_VIRTUAL_STORE_PATH_ENV] = EXPECT_VIRTUAL_STORE_PATH; + + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + + expect(rushConfiguration.packageManager).toEqual('pnpm'); + expect(rushConfiguration.pnpmOptions.pnpmStore).toEqual('local'); + expect(rushConfiguration.pnpmOptions.pnpmVirtualStorePath).toEqual(EXPECT_VIRTUAL_STORE_PATH); + expect(path.isAbsolute(rushConfiguration.pnpmOptions.pnpmVirtualStorePath)).toEqual(true); + }); }); describe('Loading repo/rush-pnpm-global.json', () => { @@ -285,6 +320,18 @@ describe(RushConfiguration.name, () => { expect(rushConfiguration.pnpmOptions.pnpmStore).toEqual('global'); expect(rushConfiguration.pnpmOptions.pnpmStorePath).toEqual(EXPECT_STORE_PATH); }); + + it('loads the correct virtual store path when environment variable is defined', () => { + const EXPECT_STORE_PATH: string = path.resolve('/var/temp'); + process.env[PNPM_VIRTUAL_STORE_PATH_ENV] = EXPECT_STORE_PATH; + + const rushConfiguration: RushConfiguration = + RushConfiguration.loadFromConfigurationFile(RUSH_JSON_FILENAME); + + expect(rushConfiguration.packageManager).toEqual('pnpm'); + expect(rushConfiguration.pnpmOptions.pnpmStore).toEqual('global'); + expect(rushConfiguration.pnpmOptions.pnpmVirtualStorePath).toEqual(EXPECT_STORE_PATH); + }); }); it(`throws an error when invalid pnpmStore is defined`, () => { diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index f054e4bbe00..f2dd686f278 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -402,6 +402,13 @@ export class RushPnpmCommandLineParser { pnpmEnvironmentMap.set('NPM_CONFIG_STATE_DIR', rushConfiguration.pnpmOptions.pnpmStorePath); } + if (rushConfiguration.pnpmOptions.pnpmVirtualStorePath) { + pnpmEnvironmentMap.set( + 'NPM_CONFIG_VIRTUAL_STORE_DIR', + rushConfiguration.pnpmOptions.pnpmVirtualStorePath + ); + } + if (rushConfiguration.pnpmOptions.environmentVariables) { for (const [envKey, { value: envValue, override }] of Object.entries( rushConfiguration.pnpmOptions.environmentVariables diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index e42e37157f6..e4659db6f4e 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -854,6 +854,11 @@ ${gitLfsHookHandling} } } + // This will be overridden by RUSH_PNPM_VIRTUAL_STORE_PATH + if (EnvironmentConfiguration.pnpmVirtualStorePathOverride) { + args.push('--virtual-store-dir', this.rushConfiguration.pnpmOptions.pnpmVirtualStorePath); + } + const { pnpmVerifyStoreIntegrity } = EnvironmentConfiguration; if (pnpmVerifyStoreIntegrity !== undefined) { args.push(`--verify-store-integrity`, `${pnpmVerifyStoreIntegrity}`); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 8308aef00c8..50b748d5e67 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -86,7 +86,8 @@ export class WorkspaceInstallManager extends BaseInstallManager { if (EnvironmentConfiguration.rushTempFolderOverride !== undefined) { throw new Error( 'The RUSH_TEMP_FOLDER environment variable is not compatible with workspace installs. If attempting ' + - 'to move the PNPM store path, see the `RUSH_PNPM_STORE_PATH` environment variable.' + 'to move the PNPM store path or virtual store path, see the `RUSH_PNPM_STORE_PATH` and ' + + '`RUSH_PNPM_VIRTUAL_STORE_PATH` environment variables, respectively.' ); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index bc8ba24a594..d3e20c1e4b2 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -209,6 +209,13 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public readonly pnpmStorePath: string; + /** + * The path for PNPM to use as the virtual store directory. + * + * Will be overridden by environment variable RUSH_PNPM_VIRTUAL_STORE_PATH + */ + public readonly pnpmVirtualStorePath: string; + /** * If true, then Rush will add the "--strict-peer-dependencies" option when invoking PNPM. * @@ -408,6 +415,11 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration } else { this.pnpmStorePath = `${commonTempFolder}/pnpm-store`; } + if (EnvironmentConfiguration.pnpmVirtualStorePathOverride) { + this.pnpmVirtualStorePath = EnvironmentConfiguration.pnpmVirtualStorePathOverride; + } else { + this.pnpmVirtualStorePath = `${commonTempFolder}/.pnpm`; + } this.strictPeerDependencies = !!json.strictPeerDependencies; this.preventManualShrinkwrapChanges = !!json.preventManualShrinkwrapChanges; this.useWorkspaces = !!json.useWorkspaces; diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 3193f069b88..131c77a1a30 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -21,7 +21,7 @@ }, "pnpmStore": { - "description": "Specifies the location of the PNPM store. There are two possible values:\n\n\"local\" - use the \"pnpm-store\" folder in the current configured temp folder: \"common/temp/pnpm-store\" by default.\n\"global\" - use PNPM's global store, which has the benefit of being shared across multiple repo folders, but the disadvantage of less isolation for builds (e.g. bugs or incompatibilities when two repos use different releases of PNPM)\n\nIn both cases, the store path can be overridden by the environment variable RUSH_PNPM_STORE_PATH.\n\nThe default value is \"local\".", + "description": "Specifies the location of the PNPM store. There are two possible values:\n\n\"local\" - use the \"pnpm-store\" folder in the current configured temp folder: \"common/temp/pnpm-store\" by default.\n\"global\" - use PNPM's global store, which has the benefit of being shared across multiple repo folders, but the disadvantage of less isolation for builds (e.g. bugs or incompatibilities when two repos use different releases of PNPM)\n\nIn both cases, the store path and the virtual store path can be overridden by the environment variables RUSH_PNPM_STORE_PATH and RUSH_PNPM_VIRTUAL_STORE_PATH, respectively.\n\nThe default value is \"local\".", "type": "string", "enum": ["local", "global"] }, diff --git a/libraries/rush-lib/src/schemas/rush.schema.json b/libraries/rush-lib/src/schemas/rush.schema.json index dce5fcaae37..6acfc2a5a00 100644 --- a/libraries/rush-lib/src/schemas/rush.schema.json +++ b/libraries/rush-lib/src/schemas/rush.schema.json @@ -95,7 +95,7 @@ "type": "object", "properties": { "pnpmStore": { - "description": "Specifies the location of the PNPM store. There are two possible values:\n\n\"local\" - use the \"pnpm-store\" folder in the current configured temp folder: \"common/temp/pnpm-store\" by default.\n\"global\" - use PNPM's global store, which has the benefit of being shared across multiple repo folders, but the disadvantage of less isolation for builds (e.g. bugs or incompatibilities when two repos use different releases of PNPM)\n\nIn all cases, the store path will be overridden by the environment variable RUSH_PNPM_STORE_PATH.\n\nThe default value is \"local\".", + "description": "Specifies the location of the PNPM store. There are two possible values:\n\n\"local\" - use the \"pnpm-store\" folder in the current configured temp folder: \"common/temp/pnpm-store\" by default.\n\"global\" - use PNPM's global store, which has the benefit of being shared across multiple repo folders, but the disadvantage of less isolation for builds (e.g. bugs or incompatibilities when two repos use different releases of PNPM)\n\nIn all cases, the store path and the virtual store path will be overridden by the environment variables RUSH_PNPM_STORE_PATH and RUSH_PNPM_VIRTUAL_STORE_PATH, respectively.\n\nThe default value is \"local\".", "type": "string", "enum": ["local", "global"] },