diff --git a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts index e63bec03e7d..e04155d31a4 100644 --- a/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts +++ b/apps/lockfile-explorer/src/cli/lint/actions/CheckAction.ts @@ -102,7 +102,7 @@ export class CheckAction extends CommandLineAction { const projectFolder: string = project.projectFolder; const subspace: Subspace = project.subspace; - const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilename(); + const shrinkwrapFilename: string = subspace.getCommittedShrinkwrapFilePath(); let doc: Lockfile | LockfileV6; if (this._docMap.has(shrinkwrapFilename)) { doc = this._docMap.get(shrinkwrapFilename)!; diff --git a/common/changes/@microsoft/rush/bring-back-variants_2024-09-18-21-01.json b/common/changes/@microsoft/rush/bring-back-variants_2024-09-18-21-01.json new file mode 100644 index 00000000000..4812498c1f6 --- /dev/null +++ b/common/changes/@microsoft/rush/bring-back-variants_2024-09-18-21-01.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Bring back the Variants feature that was removed in https://github.com/microsoft/rushstack/pull/4538.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@rushstack/lockfile-explorer/bring-back-variants_2024-09-19-19-58.json b/common/changes/@rushstack/lockfile-explorer/bring-back-variants_2024-09-19-19-58.json new file mode 100644 index 00000000000..577a9d62ef7 --- /dev/null +++ b/common/changes/@rushstack/lockfile-explorer/bring-back-variants_2024-09-19-19-58.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/lockfile-explorer", + "comment": "Update to use a new API from rush-sdk.", + "type": "minor" + } + ], + "packageName": "@rushstack/lockfile-explorer" +} \ No newline at end of file diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index f4729b376b4..31cb1da9f8a 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -18,6 +18,7 @@ import { IPackageJson } from '@rushstack/node-core-library'; import { IPrefixMatch } from '@rushstack/lookup-by-path'; import { ITerminal } from '@rushstack/terminal'; import { ITerminalProvider } from '@rushstack/terminal'; +import { JsonNull } from '@rushstack/node-core-library'; import { JsonObject } from '@rushstack/node-core-library'; import { LookupByPath } from '@rushstack/lookup-by-path'; import { PackageNameParser } from '@rushstack/node-core-library'; @@ -128,7 +129,7 @@ export class CommonVersionsConfiguration { getAllPreferredVersions(): Map; getPreferredVersionsHash(): string; readonly implicitlyPreferredVersions: boolean | undefined; - static loadFromFile(jsonFilename: string, rushConfiguration?: RushConfiguration): CommonVersionsConfiguration; + static loadFromFile(jsonFilePath: string, rushConfiguration?: RushConfiguration): CommonVersionsConfiguration; readonly preferredVersions: Map; save(): boolean; } @@ -258,6 +259,7 @@ export const EnvironmentVariableNames: { readonly RUSH_PREVIEW_VERSION: "RUSH_PREVIEW_VERSION"; readonly RUSH_ALLOW_UNSUPPORTED_NODEJS: "RUSH_ALLOW_UNSUPPORTED_NODEJS"; readonly RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD: "RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD"; + readonly RUSH_VARIANT: "RUSH_VARIANT"; readonly RUSH_PARALLELISM: "RUSH_PARALLELISM"; readonly RUSH_ABSOLUTE_SYMLINKS: "RUSH_ABSOLUTE_SYMLINKS"; readonly RUSH_PNPM_STORE_PATH: "RUSH_PNPM_STORE_PATH"; @@ -510,6 +512,8 @@ export interface IGetChangedProjectsOptions { targetBranchName: string; // (undocumented) terminal: ITerminal; + // (undocumented) + variant?: string; } // @beta @@ -1126,7 +1130,7 @@ export class RepoStateFile { get packageJsonInjectedDependenciesHash(): string | undefined; get pnpmShrinkwrapHash(): string | undefined; get preferredVersionsHash(): string | undefined; - refreshState(rushConfiguration: RushConfiguration, subspace: Subspace | undefined): boolean; + refreshState(rushConfiguration: RushConfiguration, subspace: Subspace | undefined, variant?: string): boolean; } // @public @@ -1159,6 +1163,11 @@ export class RushConfiguration { readonly commonTempFolder: string; // @deprecated get commonVersions(): CommonVersionsConfiguration; + readonly currentVariantJsonFilePath: string; + // Warning: (ae-forgotten-export) The symbol "ICurrentVariantJson" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + _currentVariantJsonLoadingPromise: Promise | undefined; // @beta readonly customTipsConfiguration: CustomTipsConfiguration; // @beta @@ -1176,14 +1185,15 @@ export class RushConfiguration { findProjectByShorthandName(shorthandProjectName: string): RushConfigurationProject | undefined; findProjectByTempName(tempProjectName: string): RushConfigurationProject | undefined; // @deprecated (undocumented) - getCommittedShrinkwrapFilename(subspace?: Subspace): string; + getCommittedShrinkwrapFilename(subspace?: Subspace, variant?: string): string; // @deprecated (undocumented) - getCommonVersions(subspace?: Subspace): CommonVersionsConfiguration; + getCommonVersions(subspace?: Subspace, variant?: string): CommonVersionsConfiguration; // @deprecated (undocumented) - getCommonVersionsFilePath(subspace?: Subspace): string; - getImplicitlyPreferredVersions(subspace?: Subspace): Map; + getCommonVersionsFilePath(subspace?: Subspace, variant?: string): string; + getCurrentlyInstalledVariantAsync(): Promise; + getImplicitlyPreferredVersions(subspace?: Subspace, variant?: string): Map; // @deprecated (undocumented) - getPnpmfilePath(subspace?: Subspace): string; + getPnpmfilePath(subspace?: Subspace, variant?: string): string; getProjectByName(projectName: string): RushConfigurationProject | undefined; // @beta (undocumented) getProjectLookupForRoot(rootPath: string): LookupByPath; @@ -1201,8 +1211,6 @@ export class RushConfiguration { readonly gitSampleEmail: string; readonly gitTagSeparator: string | undefined; readonly gitVersionBumpCommitMessage: string | undefined; - // @internal @deprecated - readonly _hasVariantsField: boolean; readonly hotfixChangeEnabled: boolean; static loadFromConfigurationFile(rushJsonFilename: string): RushConfiguration; // (undocumented) @@ -1261,6 +1269,8 @@ export class RushConfiguration { tryGetSubspace(subspaceName: string): Subspace | undefined; // (undocumented) static tryLoadFromDefaultLocation(options?: ITryFindRushJsonLocationOptions): RushConfiguration | undefined; + // @beta + readonly variants: ReadonlySet; // @beta (undocumented) readonly versionPolicyConfiguration: VersionPolicyConfiguration; // @beta (undocumented) @@ -1326,6 +1336,7 @@ export class RushConstants { static readonly commandLineFilename: 'command-line.json'; static readonly commonFolderName: 'common'; static readonly commonVersionsFilename: 'common-versions.json'; + static readonly currentVariantsFilename: 'current-variants.json'; static readonly customTipsFilename: 'custom-tips.json'; static readonly defaultMaxInstallAttempts: 1; static readonly defaultSubspaceName: 'default'; @@ -1368,6 +1379,7 @@ export class RushConstants { static readonly rushTempNpmScope: '@rush-temp'; static readonly rushTempProjectsFolderName: 'projects'; static readonly rushUserConfigurationFolderName: '.rush-user'; + static readonly rushVariantsFolderName: 'variants'; static readonly rushWebSiteUrl: 'https://rushjs.io'; static readonly subspacesConfigFilename: 'subspaces.json'; // (undocumented) @@ -1391,8 +1403,16 @@ export class _RushInternals { // @beta export class RushLifecycleHooks { - readonly afterInstall: AsyncSeriesHook<[IRushCommand, Subspace]>; - readonly beforeInstall: AsyncSeriesHook<[IGlobalCommand, Subspace]>; + readonly afterInstall: AsyncSeriesHook<[ + command: IRushCommand, + subspace: Subspace, + variant: string | undefined + ]>; + readonly beforeInstall: AsyncSeriesHook<[ + command: IGlobalCommand, + subspace: Subspace, + variant: string | undefined + ]>; readonly flushTelemetry: AsyncParallelHook<[ReadonlyArray]>; readonly initialize: AsyncSeriesHook; readonly runAnyGlobalCustomCommand: AsyncSeriesHook; @@ -1458,18 +1478,20 @@ export class Subspace { _addProject(project: RushConfigurationProject): void; // @beta contains(project: RushConfigurationProject): boolean; - // @beta + // @deprecated (undocumented) getCommittedShrinkwrapFilename(): string; // @beta - getCommonVersions(): CommonVersionsConfiguration; + getCommittedShrinkwrapFilePath(variant?: string): string; + // @beta + getCommonVersions(variant?: string): CommonVersionsConfiguration; // @beta - getCommonVersionsFilePath(): string; + getCommonVersionsFilePath(variant?: string): string; // @beta - getPackageJsonInjectedDependenciesHash(): string | undefined; + getPackageJsonInjectedDependenciesHash(variant?: string): string | undefined; // @beta - getPnpmConfigFilePath(): string; + getPnpmConfigFilePath(variant?: string): string; // @beta - getPnpmfilePath(): string; + getPnpmfilePath(variant?: string): string; // @beta getPnpmOptions(): PnpmOptionsConfiguration | undefined; // @beta @@ -1486,10 +1508,14 @@ export class Subspace { getSubspaceTempFolderPath(): string; // @beta getTempShrinkwrapFilename(): string; - // @beta + // @deprecated (undocumented) getTempShrinkwrapPreinstallFilename(subspaceName?: string | undefined): string; // @beta - get shouldEnsureConsistentVersions(): boolean; + getTempShrinkwrapPreinstallFilePath(): string; + // @beta + getVariantDependentSubspaceConfigFolderPath(variant: string | undefined): string; + // @beta + shouldEnsureConsistentVersions(variant?: string): boolean; // (undocumented) readonly subspaceName: string; } diff --git a/libraries/rush-lib/assets/rush-init/rush.json b/libraries/rush-lib/assets/rush-init/rush.json index f77555b001b..d664739144a 100644 --- a/libraries/rush-lib/assets/rush-init/rush.json +++ b/libraries/rush-lib/assets/rush-init/rush.json @@ -273,6 +273,39 @@ "postRushx": [] }, + /** + * Installation variants allow you to maintain a parallel set of configuration files that can be + * used to build the entire monorepo with an alternate set of dependencies. For example, suppose + * you upgrade all your projects to use a new release of an important framework, but during a transition period + * you intend to maintain compatibility with the old release. In this situation, you probably want your + * CI validation to build the entire repo twice: once with the old release, and once with the new release. + * + * Rush "installation variants" correspond to sets of config files located under this folder: + * + * common/config/rush/variants/ + * + * The variant folder can contain an alternate common-versions.json file. Its "preferredVersions" field can be used + * to select older versions of dependencies (within a loose SemVer range specified in your package.json files). + * To install a variant, run "rush install --variant ". + * + * For more details and instructions, see this article: https://rushjs.io/pages/advanced/installation_variants/ + */ + "variants": [ + /*[BEGIN "HYPOTHETICAL"]*/ + { + /** + * The folder name for this variant. + */ + "variantName": "old-sdk", + + /** + * An informative description + */ + "description": "Build this repo using the previous release of the SDK" + } + /*[END "HYPOTHETICAL"]*/ + ], + /** * Rush can collect anonymous telemetry about everyday developer activity such as * success/failure of installs, builds, and other operations. You can use this to identify diff --git a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts index a8343638f87..73ae342733b 100644 --- a/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts +++ b/libraries/rush-lib/src/api/CommonVersionsConfiguration.ts @@ -185,16 +185,16 @@ export class CommonVersionsConfiguration { * If the file has not been created yet, then an empty object is returned. */ public static loadFromFile( - jsonFilename: string, + jsonFilePath: string, rushConfiguration?: RushConfiguration ): CommonVersionsConfiguration { let commonVersionsJson: ICommonVersionsJson | undefined = undefined; - if (FileSystem.exists(jsonFilename)) { - commonVersionsJson = JsonFile.loadAndValidate(jsonFilename, CommonVersionsConfiguration._jsonSchema); + if (FileSystem.exists(jsonFilePath)) { + commonVersionsJson = JsonFile.loadAndValidate(jsonFilePath, CommonVersionsConfiguration._jsonSchema); } - return new CommonVersionsConfiguration(commonVersionsJson, jsonFilename, rushConfiguration); + return new CommonVersionsConfiguration(commonVersionsJson, jsonFilePath, rushConfiguration); } private static _deserializeTable( diff --git a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts index 7826651d1d2..da883ddff60 100644 --- a/libraries/rush-lib/src/api/EnvironmentConfiguration.ts +++ b/libraries/rush-lib/src/api/EnvironmentConfiguration.ts @@ -50,6 +50,14 @@ export const EnvironmentVariableNames = { */ RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD: 'RUSH_ALLOW_WARNINGS_IN_SUCCESSFUL_BUILD', + /** + * This variable selects a specific installation variant for Rush to use when installing + * and linking package dependencies. + * For more information, see the command-line help for the `--variant` parameter + * and this article: https://rushjs.io/pages/advanced/installation_variants/ + */ + RUSH_VARIANT: 'RUSH_VARIANT', + /** * Specifies the maximum number of concurrent processes to launch during a build. * For more information, see the command-line help for the `--parallelism` parameter for "rush build". @@ -533,6 +541,7 @@ export class EnvironmentConfiguration { case EnvironmentVariableNames.RUSH_PARALLELISM: case EnvironmentVariableNames.RUSH_PREVIEW_VERSION: + case EnvironmentVariableNames.RUSH_VARIANT: case EnvironmentVariableNames.RUSH_DEPLOY_TARGET_FOLDER: // Handled by @microsoft/rush front end break; diff --git a/libraries/rush-lib/src/api/RushConfiguration.ts b/libraries/rush-lib/src/api/RushConfiguration.ts index 70ed8338ab0..deee29cb4ae 100644 --- a/libraries/rush-lib/src/api/RushConfiguration.ts +++ b/libraries/rush-lib/src/api/RushConfiguration.ts @@ -12,7 +12,8 @@ import { FileSystem, type PackageNameParser, type FileSystemStats, - InternalError + InternalError, + type JsonNull } from '@rushstack/node-core-library'; import { LookupByPath } from '@rushstack/lookup-by-path'; import { trueCasePathSync } from 'true-case-path'; @@ -144,6 +145,14 @@ export interface IRushRepositoryJsonMultipleUrls extends IRushRepositoryJsonBase export type IRushRepositoryJson = IRushRepositoryJsonSingleUrl | IRushRepositoryJsonMultipleUrls; +/** + * Options defining an allowed variant as part of IRushConfigurationJson. + */ +export interface IRushVariantOptionsJson { + variantName: string; + description: string; +} + /** * This represents the JSON data structure for the "rush.json" configuration file. * See rush.schema.json for documentation. @@ -173,7 +182,14 @@ export interface IRushConfigurationJson { pnpmOptions?: IPnpmOptionsJson; yarnOptions?: IYarnOptionsJson; ensureConsistentVersions?: boolean; - variants?: unknown; + variants?: IRushVariantOptionsJson[]; +} + +/** + * This represents the JSON data structure for the "current-variant.json" data file. + */ +export interface ICurrentVariantJson { + variant: string | JsonNull; } /** @@ -212,6 +228,11 @@ export class RushConfiguration { private readonly _pathTrees: Map>; + /** + * @internal + */ + public _currentVariantJsonLoadingPromise: Promise | undefined; + // Lazily loaded when the projects() getter is called. private _projects: RushConfigurationProject[] | undefined; @@ -340,12 +361,13 @@ export class RushConfiguration { public readonly subspacesFeatureEnabled: boolean; /** - * If true, the `variants` field is present in rush.json. + * The filename of the variant dependency data file. By default this is + * called 'current-variant.json' and resides in the Rush common folder. + * Its data structure is defined by ICurrentVariantJson. * - * @internal - * @deprecated - Remove when the field is removed from the rush.json schema. + * Example: `C:\MyRepo\common\temp\current-variant.json` */ - public readonly _hasVariantsField: boolean; + public readonly currentVariantJsonFilePath: string; /** * The version of the locally package manager tool. (Example: "1.2.3") @@ -564,6 +586,13 @@ export class RushConfiguration { */ public readonly _rushPluginsConfiguration: RushPluginsConfiguration; + /** + * The variants specified in the rush.json configuration file. + * + * @beta + */ + public readonly variants: ReadonlySet; + /** * Use RushConfiguration.loadFromConfigurationFile() or Use RushConfiguration.loadFromDefaultLocation() * instead. @@ -617,6 +646,8 @@ export class RushConfiguration { this.changesFolder = path.join(this.commonFolder, RushConstants.changeFilesFolderName); + this.currentVariantJsonFilePath = path.join(this.commonTempFolder, RushConstants.currentVariantsFilename); + this.suppressNodeLtsWarning = !!rushConfigurationJson.suppressNodeLtsWarning; this._ensureConsistentVersionsJsonValue = rushConfigurationJson.ensureConsistentVersions; @@ -826,7 +857,18 @@ export class RushConfiguration { ); this.customTipsConfiguration = new CustomTipsConfiguration(this.customTipsConfigurationFilePath); - this._hasVariantsField = !!rushConfigurationJson.variants; + const variants: Set = new Set(); + for (const variantOptions of rushConfigurationJson.variants ?? []) { + const { variantName } = variantOptions; + + if (variants.has(variantName)) { + throw new Error(`Duplicate variant named '${variantName}' specified in configuration.`); + } + + variants.add(variantName); + } + + this.variants = variants; this._pathTrees = new Map(); } @@ -1372,39 +1414,57 @@ export class RushConfiguration { * Instead it will be initialized in an empty state, and calling CommonVersionsConfiguration.save() * will create the file. * - * @deprecated Use `getCommonVersions` instead, which gets the correct common version data. + * @deprecated Use `getCommonVersions` instead, which gets the correct common version data + * for a given active variant. */ public get commonVersions(): CommonVersionsConfiguration { - return this.defaultSubspace.getCommonVersions(); + return this.defaultSubspace.getCommonVersions(undefined); + } + + /** + * Gets the currently-installed variant, if an installation has occurred. + * For Rush operations which do not take a --variant parameter, this method + * determines which variant, if any, was last specified when performing "rush install" + * or "rush update". + */ + public async getCurrentlyInstalledVariantAsync(): Promise { + if (!this._currentVariantJsonLoadingPromise) { + this._currentVariantJsonLoadingPromise = this._loadCurrentVariantJsonAsync(); + } + + return (await this._currentVariantJsonLoadingPromise)?.variant ?? undefined; } /** * @deprecated Use {@link Subspace.getCommonVersionsFilePath} instead */ - public getCommonVersionsFilePath(subspace?: Subspace): string { - return (subspace ?? this.defaultSubspace).getCommonVersionsFilePath(); + public getCommonVersionsFilePath(subspace?: Subspace, variant?: string): string { + return (subspace ?? this.defaultSubspace).getCommonVersionsFilePath(variant); } /** * @deprecated Use {@link Subspace.getCommonVersions} instead */ - public getCommonVersions(subspace?: Subspace): CommonVersionsConfiguration { - return (subspace ?? this.defaultSubspace).getCommonVersions(); + public getCommonVersions(subspace?: Subspace, variant?: string): CommonVersionsConfiguration { + return (subspace ?? this.defaultSubspace).getCommonVersions(variant); } /** * Returns a map of all direct dependencies that only have a single semantic version specifier. * + * @param subspace - The subspace to use + * @param variant - The name of the current variant in use by the active command. + * * @returns A map of dependency name --\> version specifier for implicitly preferred versions. */ - public getImplicitlyPreferredVersions(subspace?: Subspace): Map { + public getImplicitlyPreferredVersions(subspace?: Subspace, variant?: string): Map { // TODO: During the next major release of Rush, replace this `require` call with a dynamic import, and // change this function to be async. const DependencyAnalyzerModule: typeof DependencyAnalyzerModuleType = require('../logic/DependencyAnalyzer'); const dependencyAnalyzer: DependencyAnalyzerModuleType.DependencyAnalyzer = DependencyAnalyzerModule.DependencyAnalyzer.forRushConfiguration(this); const dependencyAnalysis: DependencyAnalyzerModuleType.IDependencyAnalysis = - dependencyAnalyzer.getAnalysis(subspace); + dependencyAnalyzer.getAnalysis(subspace, variant, false); return dependencyAnalysis.implicitlyPreferredVersionByPackageName; } @@ -1423,17 +1483,17 @@ export class RushConfiguration { } /** - * @deprecated Use {@link Subspace.getCommittedShrinkwrapFilename} instead + * @deprecated Use {@link Subspace.getCommittedShrinkwrapFilePath} instead */ - public getCommittedShrinkwrapFilename(subspace?: Subspace): string { - return (subspace ?? this.defaultSubspace).getCommittedShrinkwrapFilename(); + public getCommittedShrinkwrapFilename(subspace?: Subspace, variant?: string): string { + return (subspace ?? this.defaultSubspace).getCommittedShrinkwrapFilePath(variant); } /** - * @deprecated Use {@link Subspace.getRepoStateFilePath} instead + * @deprecated Use {@link Subspace.getPnpmfilePath} instead */ - public getPnpmfilePath(subspace?: Subspace): string { - return (subspace ?? this.defaultSubspace).getPnpmfilePath(); + public getPnpmfilePath(subspace?: Subspace, variant?: string): string { + return (subspace ?? this.defaultSubspace).getPnpmfilePath(variant); } /** @@ -1518,4 +1578,14 @@ export class RushConfiguration { } return undefined; } + + private async _loadCurrentVariantJsonAsync(): Promise { + try { + return await JsonFile.loadAsync(this.currentVariantJsonFilePath); + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; + } + } + } } diff --git a/libraries/rush-lib/src/api/Subspace.ts b/libraries/rush-lib/src/api/Subspace.ts index 239feed81bc..0d08f19489f 100644 --- a/libraries/rush-lib/src/api/Subspace.ts +++ b/libraries/rush-lib/src/api/Subspace.ts @@ -30,8 +30,8 @@ interface ISubspaceDetail { subspaceConfigFolderPath: string; subspacePnpmPatchesFolderPath: string; subspaceTempFolderPath: string; - tempShrinkwrapFilename: string; - tempShrinkwrapPreinstallFilename: string; + tempShrinkwrapFilePath: string; + tempShrinkwrapPreinstallFilePath: string; } interface IPackageJsonLite extends Omit {} @@ -171,34 +171,53 @@ export class Subspace { let subspaceTempFolderPath: string; if (rushConfiguration.subspacesFeatureEnabled) { // Example: C:\MyRepo\common\temp\my-subspace - subspaceTempFolderPath = path.join(commonTempFolder, this.subspaceName); + subspaceTempFolderPath = `${commonTempFolder}/${this.subspaceName}`; } else { // Example: C:\MyRepo\common\temp subspaceTempFolderPath = commonTempFolder; } // Example: C:\MyRepo\common\temp\my-subspace\pnpm-lock.yaml - const tempShrinkwrapFilename: string = - subspaceTempFolderPath + `/${rushConfiguration.shrinkwrapFilename}`; + const tempShrinkwrapFilePath: string = `${subspaceTempFolderPath}/${rushConfiguration.shrinkwrapFilename}`; /// From "C:\MyRepo\common\temp\pnpm-lock.yaml" --> "C:\MyRepo\common\temp\pnpm-lock-preinstall.yaml" - const parsedPath: path.ParsedPath = path.parse(tempShrinkwrapFilename); - const tempShrinkwrapPreinstallFilename: string = path.join( - parsedPath.dir, - parsedPath.name + '-preinstall' + parsedPath.ext - ); + const parsedPath: path.ParsedPath = path.parse(tempShrinkwrapFilePath); + const tempShrinkwrapPreinstallFilePath: string = `${parsedPath.dir}/${parsedPath.name}-preinstall${parsedPath.ext}`; this._detail = { subspaceConfigFolderPath, subspacePnpmPatchesFolderPath, subspaceTempFolderPath, - tempShrinkwrapFilename, - tempShrinkwrapPreinstallFilename + tempShrinkwrapFilePath, + tempShrinkwrapPreinstallFilePath }; } return this._detail; } + /** + * Returns the full path of the folder containing this subspace's variant-dependent configuration files + * such as `pnpm-lock.yaml`. + * + * Example: `common/config/subspaces/my-subspace` or `common/config/subspaces/my-subspace/variants/my-variant` + * @beta + * + * @remarks + * The following files may be variant-dependent: + * - Lockfiles: (i.e. - `pnpm-lock.yaml`, `npm-shrinkwrap.json`, `yarn.lock`, etc) + * - 'common-versions.json' + * - 'pnpmfile.js'/'.pnpmfile.cjs' + * - 'pnpm-config.js' + */ + public getVariantDependentSubspaceConfigFolderPath(variant: string | undefined): string { + const subspaceConfigFolderPath: string = this.getSubspaceConfigFolderPath(); + if (!variant) { + return subspaceConfigFolderPath; + } else { + return `${subspaceConfigFolderPath}/${RushConstants.rushVariantsFolderName}/${variant}`; + } + } + /** * Returns the full path of the folder containing this subspace's configuration files such as `pnpm-lock.yaml`. * @@ -242,7 +261,14 @@ export class Subspace { * @beta */ public getTempShrinkwrapFilename(): string { - return this._ensureDetail().tempShrinkwrapFilename; + return this._ensureDetail().tempShrinkwrapFilePath; + } + + /** + * @deprecated - Use {@link Subspace.getTempShrinkwrapPreinstallFilePath} instead. + */ + public getTempShrinkwrapPreinstallFilename(subspaceName?: string | undefined): string { + return this.getTempShrinkwrapPreinstallFilePath(); } /** @@ -255,8 +281,8 @@ export class Subspace { * or `C:\MyRepo\common\temp\pnpm-lock-preinstall.yaml` * @beta */ - public getTempShrinkwrapPreinstallFilename(subspaceName?: string | undefined): string { - return this._ensureDetail().tempShrinkwrapPreinstallFilename; + public getTempShrinkwrapPreinstallFilePath(): string { + return this._ensureDetail().tempShrinkwrapPreinstallFilePath; } /** @@ -265,8 +291,10 @@ export class Subspace { * Example: `C:\MyRepo\common\subspaces\my-subspace\common-versions.json` * @beta */ - public getCommonVersionsFilePath(): string { - return this._ensureDetail().subspaceConfigFolderPath + '/' + RushConstants.commonVersionsFilename; + public getCommonVersionsFilePath(variant?: string): string { + return ( + this.getVariantDependentSubspaceConfigFolderPath(variant) + '/' + RushConstants.commonVersionsFilename + ); } /** @@ -275,19 +303,19 @@ export class Subspace { * Example: `C:\MyRepo\common\subspaces\my-subspace\pnpm-config.json` * @beta */ - public getPnpmConfigFilePath(): string { - return this._ensureDetail().subspaceConfigFolderPath + '/' + RushConstants.pnpmConfigFilename; + public getPnpmConfigFilePath(variant?: string): string { + return this.getVariantDependentSubspaceConfigFolderPath(variant) + '/' + RushConstants.pnpmConfigFilename; } /** * Gets the settings from the common-versions.json config file. * @beta */ - public getCommonVersions(): CommonVersionsConfiguration { - const commonVersionsFilename: string = this.getCommonVersionsFilePath(); + public getCommonVersions(variant?: string): CommonVersionsConfiguration { + const commonVersionsFilePath: string = this.getCommonVersionsFilePath(variant); if (!this._commonVersionsConfiguration) { this._commonVersionsConfiguration = CommonVersionsConfiguration.loadFromFile( - commonVersionsFilename, + commonVersionsFilePath, this._rushConfiguration ); } @@ -299,13 +327,13 @@ export class Subspace { * or from the rush.json file if it isn't defined in common-versions.json * @beta */ - public get shouldEnsureConsistentVersions(): boolean { + public shouldEnsureConsistentVersions(variant?: string): boolean { // If the subspaces feature is enabled, or the ensureConsistentVersions field is defined, return the value of the field - if ( - this._rushConfiguration.subspacesFeatureEnabled || - this.getCommonVersions().ensureConsistentVersions !== undefined - ) { - return !!this.getCommonVersions().ensureConsistentVersions; + if (this._rushConfiguration.subspacesFeatureEnabled) { + const commonVersions: CommonVersionsConfiguration = this.getCommonVersions(variant); + if (commonVersions.ensureConsistentVersions !== undefined) { + return commonVersions.ensureConsistentVersions; + } } // Fallback to ensureConsistentVersions in rush.json if subspaces is not enabled, @@ -318,7 +346,7 @@ export class Subspace { * @beta */ public getRepoStateFilePath(): string { - return this._ensureDetail().subspaceConfigFolderPath + '/' + RushConstants.repoStateFilename; + return this.getSubspaceConfigFolderPath() + '/' + RushConstants.repoStateFilename; } /** @@ -327,17 +355,25 @@ export class Subspace { * @beta */ public getRepoState(): RepoStateFile { - const repoStateFilename: string = this.getRepoStateFilePath(); - return RepoStateFile.loadFromFile(repoStateFilename); + const repoStateFilePath: string = this.getRepoStateFilePath(); + return RepoStateFile.loadFromFile(repoStateFilePath); } /** - * Gets the committed shrinkwrap file name. - * @beta + * @deprecated - Use {@link Subspace.getCommittedShrinkwrapFilePath} instead. */ public getCommittedShrinkwrapFilename(): string { - const subspaceConfigFolderPath: string = this.getSubspaceConfigFolderPath(); - return path.join(subspaceConfigFolderPath, this._rushConfiguration.shrinkwrapFilename); + return this.getCommittedShrinkwrapFilePath(undefined); + } + + /** + * Gets the committed shrinkwrap file name for a specific variant. + * @param variant - The name of the current variant in use by the active command. + * @beta + */ + public getCommittedShrinkwrapFilePath(variant?: string): string { + const subspaceConfigFolderPath: string = this.getVariantDependentSubspaceConfigFolderPath(variant); + return `${subspaceConfigFolderPath}/${this._rushConfiguration.shrinkwrapFilename}`; } /** @@ -347,13 +383,13 @@ export class Subspace { * The file path is returned even if PNPM is not configured as the package manager. * @beta */ - public getPnpmfilePath(): string { - const subspaceConfigFolderPath: string = this.getSubspaceConfigFolderPath(); + public getPnpmfilePath(variant?: string): string { + const subspaceConfigFolderPath: string = this.getVariantDependentSubspaceConfigFolderPath(variant); const pnpmFilename: string = (this._rushConfiguration.packageManagerWrapper as PnpmPackageManager) .pnpmfileFilename; - return path.join(subspaceConfigFolderPath, pnpmFilename); + return `${subspaceConfigFolderPath}/${pnpmFilename}`; } /** @@ -373,12 +409,12 @@ export class Subspace { * Returns hash value of injected dependencies in related package.json. * @beta */ - public getPackageJsonInjectedDependenciesHash(): string | undefined { + public getPackageJsonInjectedDependenciesHash(variant?: string): string | undefined { const allPackageJson: IPackageJsonLite[] = []; const relatedProjects: RushConfigurationProject[] = []; const subspacePnpmfileShimSettings: ISubspacePnpmfileShimSettings = - SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings(this._rushConfiguration, this); + SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings(this._rushConfiguration, this, variant); for (const rushProject of this.getProjects()) { const injectedDependencies: Array = diff --git a/libraries/rush-lib/src/api/Variants.ts b/libraries/rush-lib/src/api/Variants.ts new file mode 100644 index 00000000000..06825d73281 --- /dev/null +++ b/libraries/rush-lib/src/api/Variants.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { CommandLineStringParameter, ICommandLineStringDefinition } from '@rushstack/ts-command-line'; + +import { EnvironmentVariableNames } from './EnvironmentConfiguration'; +import type { RushConfiguration } from './RushConfiguration'; +import { RushConstants } from '../logic/RushConstants'; + +/** + * Provides the parameter configuration for '--variant'. + */ +export const VARIANT_PARAMETER: ICommandLineStringDefinition = { + parameterLongName: '--variant', + argumentName: 'VARIANT', + description: 'Run command using a variant installation configuration', + environmentVariable: EnvironmentVariableNames.RUSH_VARIANT +}; + +export async function getVariantAsync( + variantsParameter: CommandLineStringParameter | undefined, + rushConfiguration: RushConfiguration, + defaultToCurrentlyInstalledVariant: boolean +): Promise { + let variant: string | undefined = variantsParameter?.value; + if (variant && !rushConfiguration.variants.has(variant)) { + throw new Error(`The variant "${variant}" is not defined in ${RushConstants.rushJsonFilename}`); + } + + if (!variant && defaultToCurrentlyInstalledVariant) { + variant = await rushConfiguration.getCurrentlyInstalledVariantAsync(); + } + + return variant; +} diff --git a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts index b20596e935f..f5edac12662 100644 --- a/libraries/rush-lib/src/api/test/RushConfiguration.test.ts +++ b/libraries/rush-lib/src/api/test/RushConfiguration.test.ts @@ -118,7 +118,7 @@ describe(RushConfiguration.name, () => { expect(rushConfiguration.shrinkwrapFilename).toEqual('pnpm-lock.yaml'); assertPathProperty( 'getPnpmfilePath', - rushConfiguration.getPnpmfilePath(), + rushConfiguration.defaultSubspace.getPnpmfilePath(undefined), './repo/common/config/rush/.pnpmfile.cjs' ); assertPathProperty('commonFolder', rushConfiguration.commonFolder, './repo/common'); @@ -185,7 +185,7 @@ describe(RushConfiguration.name, () => { expect(rushConfiguration.shrinkwrapFilename).toEqual('pnpm-lock.yaml'); assertPathProperty( 'getPnpmfilePath', - rushConfiguration.getPnpmfilePath(), + rushConfiguration.defaultSubspace.getPnpmfilePath(undefined), './repo/common/config/rush/pnpmfile.js' ); expect(rushConfiguration.repositoryUrls).toEqual(['someFakeUrl', 'otherFakeUrl']); diff --git a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap index 991aa424551..f36ce6efc6f 100644 --- a/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap +++ b/libraries/rush-lib/src/api/test/__snapshots__/RushCommandLine.test.ts.snap @@ -70,6 +70,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, ], }, Object { @@ -184,6 +192,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, ], }, Object { @@ -382,6 +398,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, Object { "description": "Normally all projects in the monorepo will be processed; adding this parameter will instead select a subset of projects. Each \\"--to\\" parameter expands this selection to include PROJECT and all its dependencies. \\".\\" can be used as shorthand for the project in the current working directory. For details, refer to the website article \\"Selecting subsets of projects\\".", "environmentVariable": undefined, @@ -799,6 +823,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, ], }, Object { @@ -897,6 +929,14 @@ Object { "required": false, "shortName": undefined, }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, Object { "description": "Normally \\"rush update\\" tries to preserve your existing installed versions and only makes the minimum updates needed to satisfy the package.json files. This conservative approach prevents your PR from getting involved with package updates that are unrelated to your work. Use \\"--full\\" when you really want to update all dependencies to the latest SemVer-compatible version. This should be done periodically by a person or robot whose role is to deal with potential upgrade regressions.", "environmentVariable": undefined, @@ -989,6 +1029,14 @@ Object { "required": false, "shortName": "-s", }, + Object { + "description": "Run command using a variant installation configuration", + "environmentVariable": "RUSH_VARIANT", + "kind": "String", + "longName": "--variant", + "required": false, + "shortName": undefined, + }, ], }, Object { diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 2f404435424..f054e4bbe00 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -32,6 +32,7 @@ import { objectsAreDeepEqual } from '../utilities/objectUtilities'; import { Utilities } from '../utilities/Utilities'; import type { Subspace } from '../api/Subspace'; import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration'; +import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration'; const RUSH_SKIP_CHECKS_PARAMETER: string = '--rush-skip-checks'; @@ -460,7 +461,6 @@ export class RushPnpmCommandLineParser { } const subspaceTempFolder: string = this._subspace.getSubspaceTempFolderPath(); - const subspaceConfigFolder: string = this._subspace.getSubspaceConfigFolderPath(); switch (commandName) { case 'patch-remove': @@ -470,6 +470,7 @@ export class RushPnpmCommandLineParser { // 2. we can not fallback to use Monorepo config folder (common/config/rush) due to that this command is intended to apply to input subspace only. // It will produce unexpected behavior if we use the fallback. if (this._subspace.getPnpmOptions() === undefined) { + const subspaceConfigFolder: string = this._subspace.getSubspaceConfigFolderPath(); this._terminal.writeErrorLine( `The "rush-pnpm patch-commit" command cannot proceed without a pnpm-config.json file.` + ` Create one in this folder: ${subspaceConfigFolder}` @@ -543,6 +544,7 @@ export class RushPnpmCommandLineParser { networkConcurrency: undefined, offline: false, collectLogFile: false, + variant: process.env[EnvironmentVariableNames.RUSH_VARIANT], // For `rush-pnpm`, only use the env var maxInstallAttempts: RushConstants.defaultMaxInstallAttempts, pnpmFilterArgumentValues: [], selectedProjects: new Set(this._rushConfiguration.projects), diff --git a/libraries/rush-lib/src/cli/actions/AddAction.ts b/libraries/rush-lib/src/cli/actions/AddAction.ts index eca166513a8..f0c2fe25593 100644 --- a/libraries/rush-lib/src/cli/actions/AddAction.ts +++ b/libraries/rush-lib/src/cli/actions/AddAction.ts @@ -2,7 +2,11 @@ // See LICENSE in the project root for license information. import * as semver from 'semver'; -import type { CommandLineFlagParameter, CommandLineStringListParameter } from '@rushstack/ts-command-line'; +import type { + CommandLineFlagParameter, + CommandLineStringListParameter, + CommandLineStringParameter +} from '@rushstack/ts-command-line'; import { BaseAddAndRemoveAction } from './BaseAddAndRemoveAction'; import type { RushCommandLineParser } from '../RushCommandLineParser'; @@ -13,6 +17,7 @@ import { type IPackageJsonUpdaterRushAddOptions, SemVerStyle } from '../../logic/PackageJsonUpdaterTypes'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; export class AddAction extends BaseAddAndRemoveAction { protected readonly _allFlag: CommandLineFlagParameter; @@ -22,6 +27,7 @@ export class AddAction extends BaseAddAndRemoveAction { private readonly _devDependencyFlag: CommandLineFlagParameter; private readonly _peerDependencyFlag: CommandLineFlagParameter; private readonly _makeConsistentFlag: CommandLineFlagParameter; + private readonly _variantParameter: CommandLineStringParameter; public constructor(parser: RushCommandLineParser) { const documentation: string = [ @@ -85,9 +91,10 @@ export class AddAction extends BaseAddAndRemoveAction { parameterLongName: '--all', description: 'If specified, the dependency will be added to all projects.' }); + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } - public getUpdateOptions(): IPackageJsonUpdaterRushAddOptions { + public async getUpdateOptionsAsync(): Promise { const projects: RushConfigurationProject[] = super.getProjects(); if (this._caretFlag.value && this._exactFlag.value) { @@ -149,6 +156,13 @@ export class AddAction extends BaseAddAndRemoveAction { packagesToAdd.push({ packageName, version, rangeStyle }); } + + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + true + ); + return { projects: projects, packagesToUpdate: packagesToAdd, @@ -157,7 +171,8 @@ export class AddAction extends BaseAddAndRemoveAction { updateOtherPackages: this._makeConsistentFlag.value, skipUpdate: this._skipUpdateFlag.value, debugInstall: this.parser.isDebug, - actionName: this.actionName + actionName: this.actionName, + variant }; } } diff --git a/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts b/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts index 1398164a096..71b84b3cd39 100644 --- a/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts +++ b/libraries/rush-lib/src/cli/actions/BaseAddAndRemoveAction.ts @@ -54,7 +54,7 @@ export abstract class BaseAddAndRemoveAction extends BaseRushAction { }); } - protected abstract getUpdateOptions(): IPackageJsonUpdaterRushBaseUpdateOptions; + protected abstract getUpdateOptionsAsync(): Promise; protected getProjects(): RushConfigurationProject[] { if (this._allFlag.value) { @@ -83,6 +83,7 @@ export abstract class BaseAddAndRemoveAction extends BaseRushAction { this.rushGlobalFolder ); - await updater.doRushUpdateAsync(this.getUpdateOptions()); + const updateOptions: IPackageJsonUpdaterRushBaseUpdateOptions = await this.getUpdateOptionsAsync(); + await updater.doRushUpdateAsync(updateOptions); } } diff --git a/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts b/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts index 413089788b7..82865908a69 100644 --- a/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts +++ b/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts @@ -4,6 +4,7 @@ import type { CommandLineFlagParameter, CommandLineIntegerParameter, + CommandLineStringParameter, IRequiredCommandLineIntegerParameter } from '@rushstack/ts-command-line'; import { AlreadyReportedError } from '@rushstack/node-core-library'; @@ -22,6 +23,7 @@ import { RushConstants } from '../../logic/RushConstants'; import { SUBSPACE_LONG_ARG_NAME, type SelectionParameterSet } from '../parsing/SelectionParameterSet'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import type { Subspace } from '../../api/Subspace'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; /** * Temporary data structure used by `BaseInstallAction.runAsync()` @@ -36,6 +38,7 @@ interface ISubspaceInstallationData { */ export abstract class BaseInstallAction extends BaseRushAction { protected readonly _terminal: ITerminal; + protected readonly _variantParameter: CommandLineStringParameter; protected readonly _purgeParameter: CommandLineFlagParameter; protected readonly _bypassPolicyParameter: CommandLineFlagParameter; protected readonly _noLinkParameter: CommandLineFlagParameter; @@ -105,6 +108,7 @@ export abstract class BaseInstallAction extends BaseRushAction { ` if the necessary NPM packages cannot be obtained from the local cache.` + ` For details, see the documentation for PNPM's "--offline" parameter.` }); + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } protected abstract buildInstallOptionsAsync(): Promise>; @@ -113,16 +117,6 @@ export abstract class BaseInstallAction extends BaseRushAction { const installManagerOptions: Omit = await this.buildInstallOptionsAsync(); - if (this.rushConfiguration._hasVariantsField) { - this._terminal.writeLine( - Colorize.yellow( - `Warning: Please remove the obsolete "variants" field from your ${RushConstants.rushJsonFilename} ` + - 'file. Installation variants have been replaced by the new Rush subspaces feature. ' + - 'In the next major release, Rush will fail to execute if this field is present.' - ) - ); - } - // If we are doing a filtered install and subspaces is enabled, we need to find the affected subspaces and install for all of them. let selectedSubspaces: ReadonlySet | undefined; const subspaceInstallationDataBySubspace: Map = new Map(); @@ -175,15 +169,24 @@ export abstract class BaseInstallAction extends BaseRushAction { } } + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + false + ); if (selectedSubspaces) { // Check each subspace for version inconsistencies for (const subspace of selectedSubspaces) { VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this._terminal, { - subspace + subspace, + variant }); } } else { - VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this._terminal); + VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this._terminal, { + subspace: undefined, + variant + }); } const stopwatch: Stopwatch = Stopwatch.start(); diff --git a/libraries/rush-lib/src/cli/actions/CheckAction.ts b/libraries/rush-lib/src/cli/actions/CheckAction.ts index e922d9ca833..2a4d769bfab 100644 --- a/libraries/rush-lib/src/cli/actions/CheckAction.ts +++ b/libraries/rush-lib/src/cli/actions/CheckAction.ts @@ -2,17 +2,19 @@ // See LICENSE in the project root for license information. import type { CommandLineFlagParameter, CommandLineStringParameter } from '@rushstack/ts-command-line'; -import { ConsoleTerminalProvider, type ITerminal, Terminal } from '@rushstack/terminal'; +import { Colorize, type ITerminal } from '@rushstack/terminal'; import type { RushCommandLineParser } from '../RushCommandLineParser'; import { BaseRushAction } from './BaseRushAction'; import { VersionMismatchFinder } from '../../logic/versionMismatch/VersionMismatchFinder'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; export class CheckAction extends BaseRushAction { private readonly _terminal: ITerminal; private readonly _jsonFlag: CommandLineFlagParameter; private readonly _verboseFlag: CommandLineFlagParameter; private readonly _subspaceParameter: CommandLineStringParameter | undefined; + private readonly _variantParameter: CommandLineStringParameter; public constructor(parser: RushCommandLineParser) { super({ @@ -27,7 +29,7 @@ export class CheckAction extends BaseRushAction { parser }); - this._terminal = new Terminal(new ConsoleTerminalProvider({ verboseEnabled: parser.isDebug })); + this._terminal = parser.terminal; this._jsonFlag = this.defineFlagParameter({ parameterLongName: '--json', description: 'If this flag is specified, output will be in JSON format.' @@ -46,6 +48,7 @@ export class CheckAction extends BaseRushAction { 'consistent only within that subspace (ignoring other subspaces). This parameter is required when ' + 'the "subspacesEnabled" setting is set to true in subspaces.json.' }); + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } protected async runAsync(): Promise { @@ -54,7 +57,25 @@ export class CheckAction extends BaseRushAction { `The --subspace parameter must be specified with "rush check" when subspaces is enabled.` ); } + + const currentlyInstalledVariant: string | undefined = + await this.rushConfiguration.getCurrentlyInstalledVariantAsync(); + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + true + ); + if (!variant && currentlyInstalledVariant) { + this._terminal.writeWarningLine( + Colorize.yellow( + `Variant '${currentlyInstalledVariant}' has been installed, but 'rush check' is currently checking the default variant. ` + + `Use 'rush ${this.actionName} ${this._variantParameter.longName} '${currentlyInstalledVariant}' to check the current installation.` + ) + ); + } + VersionMismatchFinder.rushCheck(this.rushConfiguration, this._terminal, { + variant, printAsJson: this._jsonFlag.value, truncateLongPackageNameLists: !this._verboseFlag.value, subspace: this._subspaceParameter?.value diff --git a/libraries/rush-lib/src/cli/actions/DeployAction.ts b/libraries/rush-lib/src/cli/actions/DeployAction.ts index 2d0b0d1e2da..c254184840e 100644 --- a/libraries/rush-lib/src/cli/actions/DeployAction.ts +++ b/libraries/rush-lib/src/cli/actions/DeployAction.ts @@ -160,10 +160,13 @@ export class DeployAction extends BaseRushAction { const projects: RushConfigurationProject[] = this.rushConfiguration.projects; if (this.rushConfiguration.packageManager === 'pnpm') { + const currentlyInstalledVariant: string | undefined = + await this.rushConfiguration.getCurrentlyInstalledVariantAsync(); for (const project of projects) { const pnpmfileConfiguration: PnpmfileConfiguration = await PnpmfileConfiguration.initializeAsync( this.rushConfiguration, - project.subspace + project.subspace, + currentlyInstalledVariant ); const subspace: IExtractorSubspace = { subspaceName: project.subspace.subspaceName, diff --git a/libraries/rush-lib/src/cli/actions/InstallAction.ts b/libraries/rush-lib/src/cli/actions/InstallAction.ts index ceb55884326..945c12337ca 100644 --- a/libraries/rush-lib/src/cli/actions/InstallAction.ts +++ b/libraries/rush-lib/src/cli/actions/InstallAction.ts @@ -9,6 +9,7 @@ import type { RushCommandLineParser } from '../RushCommandLineParser'; import { SelectionParameterSet } from '../parsing/SelectionParameterSet'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import type { Subspace } from '../../api/Subspace'; +import { getVariantAsync } from '../../api/Variants'; export class InstallAction extends BaseInstallAction { private readonly _checkOnlyParameter: CommandLineFlagParameter; @@ -61,6 +62,12 @@ export class InstallAction extends BaseInstallAction { (await this._selectionParameters?.getSelectedProjectsAsync(this._terminal)) ?? new Set(this.rushConfiguration.projects); + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + false + ); + return { debug: this.parser.isDebug, allowShrinkwrapUpdates: false, @@ -72,6 +79,7 @@ export class InstallAction extends BaseInstallAction { offline: this._offlineParameter.value!, networkConcurrency: this._networkConcurrencyParameter.value, collectLogFile: this._debugPackageManagerParameter.value!, + variant, // Because the 'defaultValue' option on the _maxInstallAttempts parameter is set, // it is safe to assume that the value is not null maxInstallAttempts: this._maxInstallAttempts.value!, @@ -82,8 +90,9 @@ export class InstallAction extends BaseInstallAction { checkOnly: this._checkOnlyParameter.value, resolutionOnly: this._resolutionOnlyParameter?.value, beforeInstallAsync: (subspace: Subspace) => - this.rushSession.hooks.beforeInstall.promise(this, subspace), - afterInstallAsync: (subspace: Subspace) => this.rushSession.hooks.afterInstall.promise(this, subspace), + this.rushSession.hooks.beforeInstall.promise(this, subspace, variant), + afterInstallAsync: (subspace: Subspace) => + this.rushSession.hooks.afterInstall.promise(this, subspace, variant), terminal: this._terminal }; } diff --git a/libraries/rush-lib/src/cli/actions/PublishAction.ts b/libraries/rush-lib/src/cli/actions/PublishAction.ts index 90d6266534b..12170823a0f 100644 --- a/libraries/rush-lib/src/cli/actions/PublishAction.ts +++ b/libraries/rush-lib/src/cli/actions/PublishAction.ts @@ -213,9 +213,12 @@ export class PublishAction extends BaseRushAction { * Executes the publish action, which will read change request files, apply changes to package.jsons, */ protected async runAsync(): Promise { + const currentlyInstalledVariant: string | undefined = + await this.rushConfiguration.getCurrentlyInstalledVariantAsync(); await PolicyValidator.validatePolicyAsync( this.rushConfiguration, this.rushConfiguration.defaultSubspace, + currentlyInstalledVariant, { bypassPolicy: false } ); diff --git a/libraries/rush-lib/src/cli/actions/RemoveAction.ts b/libraries/rush-lib/src/cli/actions/RemoveAction.ts index 4b953fe4b54..7eb3358f8b0 100644 --- a/libraries/rush-lib/src/cli/actions/RemoveAction.ts +++ b/libraries/rush-lib/src/cli/actions/RemoveAction.ts @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { ConsoleTerminalProvider, Terminal, type ITerminal } from '@rushstack/terminal'; -import type { CommandLineFlagParameter, CommandLineStringListParameter } from '@rushstack/ts-command-line'; +import type { ITerminal } from '@rushstack/terminal'; +import type { + CommandLineFlagParameter, + CommandLineStringListParameter, + CommandLineStringParameter +} from '@rushstack/ts-command-line'; import { BaseAddAndRemoveAction } from './BaseAddAndRemoveAction'; import type { RushCommandLineParser } from '../RushCommandLineParser'; @@ -11,12 +15,13 @@ import type { IPackageForRushRemove, IPackageJsonUpdaterRushRemoveOptions } from '../../logic/PackageJsonUpdaterTypes'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; export class RemoveAction extends BaseAddAndRemoveAction { protected readonly _allFlag: CommandLineFlagParameter; protected readonly _packageNameList: CommandLineStringListParameter; - private _terminalProvider: ConsoleTerminalProvider; - private _terminal: ITerminal; + private readonly _variantParameter: CommandLineStringParameter; + private readonly _terminal: ITerminal; public constructor(parser: RushCommandLineParser) { const documentation: string = [ @@ -30,8 +35,8 @@ export class RemoveAction extends BaseAddAndRemoveAction { safeForSimultaneousRushProcesses: false, parser }); - this._terminalProvider = new ConsoleTerminalProvider(); - this._terminal = new Terminal(this._terminalProvider); + + this._terminal = parser.terminal; this._packageNameList = this.defineStringListParameter({ parameterLongName: '--package', @@ -46,9 +51,10 @@ export class RemoveAction extends BaseAddAndRemoveAction { parameterLongName: '--all', description: 'If specified, the dependency will be removed from all projects that declare it.' }); + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } - public getUpdateOptions(): IPackageJsonUpdaterRushRemoveOptions { + public async getUpdateOptionsAsync(): Promise { const projects: RushConfigurationProject[] = super.getProjects(); const packagesToRemove: IPackageForRushRemove[] = []; @@ -69,7 +75,7 @@ export class RemoveAction extends BaseAddAndRemoveAction { !project.packageJsonEditor.tryGetDevDependency(packageName) ) { this._terminal.writeLine( - `The project "${project.packageName}" do not have ${packageName} in package.json.` + `The project "${project.packageName}" does not have "${packageName}" in package.json.` ); } } @@ -77,12 +83,19 @@ export class RemoveAction extends BaseAddAndRemoveAction { packagesToRemove.push({ packageName }); } + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + true + ); + return { projects: projects, packagesToUpdate: packagesToRemove, skipUpdate: this._skipUpdateFlag.value, debugInstall: this.parser.isDebug, - actionName: this.actionName + actionName: this.actionName, + variant }; } } diff --git a/libraries/rush-lib/src/cli/actions/UpdateAction.ts b/libraries/rush-lib/src/cli/actions/UpdateAction.ts index 9cb780bf50f..837ce149c17 100644 --- a/libraries/rush-lib/src/cli/actions/UpdateAction.ts +++ b/libraries/rush-lib/src/cli/actions/UpdateAction.ts @@ -9,6 +9,7 @@ import type { RushCommandLineParser } from '../RushCommandLineParser'; import { SelectionParameterSet } from '../parsing/SelectionParameterSet'; import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import type { Subspace } from '../../api/Subspace'; +import { getVariantAsync } from '../../api/Variants'; export class UpdateAction extends BaseInstallAction { private readonly _fullParameter: CommandLineFlagParameter; @@ -84,6 +85,12 @@ export class UpdateAction extends BaseInstallAction { (await this._selectionParameters?.getSelectedProjectsAsync(this._terminal)) ?? new Set(this.rushConfiguration.projects); + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + false + ); + return { debug: this.parser.isDebug, allowShrinkwrapUpdates: true, @@ -95,6 +102,7 @@ export class UpdateAction extends BaseInstallAction { offline: this._offlineParameter.value!, networkConcurrency: this._networkConcurrencyParameter.value, collectLogFile: this._debugPackageManagerParameter.value!, + variant, // Because the 'defaultValue' option on the _maxInstallAttempts parameter is set, // it is safe to assume that the value is not null maxInstallAttempts: this._maxInstallAttempts.value!, @@ -104,8 +112,9 @@ export class UpdateAction extends BaseInstallAction { (await this._selectionParameters?.getPnpmFilterArgumentValuesAsync(this._terminal)) ?? [], checkOnly: false, beforeInstallAsync: (subspace: Subspace) => - this.rushSession.hooks.beforeInstall.promise(this, subspace), - afterInstallAsync: (subspace: Subspace) => this.rushSession.hooks.afterInstall.promise(this, subspace), + this.rushSession.hooks.beforeInstall.promise(this, subspace, variant), + afterInstallAsync: (subspace: Subspace) => + this.rushSession.hooks.afterInstall.promise(this, subspace, variant), terminal: this._terminal }; } diff --git a/libraries/rush-lib/src/cli/actions/UpgradeInteractiveAction.ts b/libraries/rush-lib/src/cli/actions/UpgradeInteractiveAction.ts index 012b457dd3b..4c1666b28a6 100644 --- a/libraries/rush-lib/src/cli/actions/UpgradeInteractiveAction.ts +++ b/libraries/rush-lib/src/cli/actions/UpgradeInteractiveAction.ts @@ -1,16 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import type { CommandLineFlagParameter, CommandLineStringParameter } from '@rushstack/ts-command-line'; import type { RushCommandLineParser } from '../RushCommandLineParser'; import { BaseRushAction } from './BaseRushAction'; import type * as PackageJsonUpdaterType from '../../logic/PackageJsonUpdater'; import type * as InteractiveUpgraderType from '../../logic/InteractiveUpgrader'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; export class UpgradeInteractiveAction extends BaseRushAction { private _makeConsistentFlag: CommandLineFlagParameter; private _skipUpdateFlag: CommandLineFlagParameter; + private readonly _variantParameter: CommandLineStringParameter; public constructor(parser: RushCommandLineParser) { const documentation: string[] = [ @@ -42,6 +44,8 @@ export class UpgradeInteractiveAction extends BaseRushAction { description: 'If specified, the "rush update" command will not be run after updating the package.json files.' }); + + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } public async runAsync(): Promise { @@ -58,17 +62,24 @@ export class UpgradeInteractiveAction extends BaseRushAction { this.rushConfiguration ); + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + true + ); const shouldMakeConsistent: boolean = - this.rushConfiguration.defaultSubspace.shouldEnsureConsistentVersions || this._makeConsistentFlag.value; + this.rushConfiguration.defaultSubspace.shouldEnsureConsistentVersions(variant) || + this._makeConsistentFlag.value; const { projects, depsToUpgrade } = await interactiveUpgrader.upgradeAsync(); await packageJsonUpdater.doRushUpgradeAsync({ - projects: projects, + projects, packagesToAdd: depsToUpgrade.packages, updateOtherPackages: shouldMakeConsistent, skipUpdate: this._skipUpdateFlag.value, - debugInstall: this.parser.isDebug + debugInstall: this.parser.isDebug, + variant }); } } diff --git a/libraries/rush-lib/src/cli/actions/VersionAction.ts b/libraries/rush-lib/src/cli/actions/VersionAction.ts index 228950d2ea1..54070750902 100644 --- a/libraries/rush-lib/src/cli/actions/VersionAction.ts +++ b/libraries/rush-lib/src/cli/actions/VersionAction.ts @@ -95,8 +95,10 @@ export class VersionAction extends BaseRushAction { } protected async runAsync(): Promise { + const currentlyInstalledVariant: string | undefined = + await this.rushConfiguration.getCurrentlyInstalledVariantAsync(); for (const subspace of this.rushConfiguration.subspaces) { - await PolicyValidator.validatePolicyAsync(this.rushConfiguration, subspace, { + await PolicyValidator.validatePolicyAsync(this.rushConfiguration, subspace, currentlyInstalledVariant, { bypassPolicyAllowed: true, bypassPolicy: this._bypassPolicy.value }); @@ -128,7 +130,7 @@ export class VersionAction extends BaseRushAction { if (updatedPackages.size > 0) { // eslint-disable-next-line no-console console.log(`${updatedPackages.size} packages are getting updated.`); - await this._gitProcessAsync(tempBranch, this._targetBranch.value); + await this._gitProcessAsync(tempBranch, this._targetBranch.value, currentlyInstalledVariant); } } else if (this._bumpVersion.value) { const tempBranch: string = 'version/bump-' + new Date().getTime(); @@ -138,7 +140,7 @@ export class VersionAction extends BaseRushAction { this._prereleaseIdentifier.value, true ); - await this._gitProcessAsync(tempBranch, this._targetBranch.value); + await this._gitProcessAsync(tempBranch, this._targetBranch.value, currentlyInstalledVariant); } } @@ -205,7 +207,7 @@ export class VersionAction extends BaseRushAction { } } - private _validateResult(): void { + private _validateResult(variant: string | undefined): void { // Load the config from file to avoid using inconsistent in-memory data. const rushConfig: RushConfiguration = RushConfiguration.loadFromConfigurationFile( this.rushConfiguration.rushJsonFile @@ -219,7 +221,8 @@ export class VersionAction extends BaseRushAction { } const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches(rushConfig, { - subspace: subspace + subspace, + variant }); if (mismatchFinder.numberOfMismatches) { throw new Error( @@ -230,9 +233,13 @@ export class VersionAction extends BaseRushAction { } } - private async _gitProcessAsync(tempBranch: string, targetBranch: string | undefined): Promise { + private async _gitProcessAsync( + tempBranch: string, + targetBranch: string | undefined, + variant: string | undefined + ): Promise { // Validate the result before commit. - this._validateResult(); + this._validateResult(variant); const git: Git = new Git(this.rushConfiguration); const publishGit: PublishGit = new PublishGit(git, targetBranch); diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 96270c35b5e..90a01d3e463 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -51,6 +51,7 @@ import { ShardedPhasedOperationPlugin } from '../../logic/operations/ShardedPhas import type { ProjectWatcher } from '../../logic/ProjectWatcher'; import { FlagFile } from '../../api/FlagFile'; import { WeightedOperationPlugin } from '../../logic/operations/WeightedOperationPlugin'; +import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; /** * Constructor parameters for PhasedScriptAction. @@ -144,6 +145,7 @@ export class PhasedScriptAction extends BaseScriptAction { private readonly _timelineParameter: CommandLineFlagParameter | undefined; private readonly _cobuildPlanParameter: CommandLineFlagParameter | undefined; private readonly _installParameter: CommandLineFlagParameter | undefined; + private readonly _variantParameter: CommandLineStringParameter | undefined; private readonly _noIPCParameter: CommandLineFlagParameter | undefined; public constructor(options: IPhasedScriptActionOptions) { @@ -258,6 +260,8 @@ export class PhasedScriptAction extends BaseScriptAction { 'Normally a phased command expects "rush install" to have been manually run first. If this flag is specified, ' + 'Rush will automatically perform an install before processing the current command.' }); + + this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER); } if ( @@ -294,14 +298,23 @@ export class PhasedScriptAction extends BaseScriptAction { '../../logic/installManager/doBasicInstallAsync' ); + const variant: string | undefined = await getVariantAsync( + this._variantParameter, + this.rushConfiguration, + true + ); await doBasicInstallAsync({ terminal: this._terminal, rushConfiguration: this.rushConfiguration, rushGlobalFolder: this.rushGlobalFolder, isDebug: this.parser.isDebug, + variant, beforeInstallAsync: (subspace: Subspace) => - this.rushSession.hooks.beforeInstall.promise(this, subspace), - afterInstallAsync: (subspace: Subspace) => this.rushSession.hooks.afterInstall.promise(this, subspace) + this.rushSession.hooks.beforeInstall.promise(this, subspace, variant), + afterInstallAsync: (subspace: Subspace) => + this.rushSession.hooks.afterInstall.promise(this, subspace, variant), + // Eventually we may want to allow a subspace to be selected here + subspace: this.rushConfiguration.defaultSubspace }); } diff --git a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index be766d0245c..007ff234fc5 100644 --- a/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/libraries/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -83,7 +83,7 @@ Optional arguments: exports[`CommandLineHelp prints the help for each action: add 1`] = ` "usage: rush add [-h] [-s] -p PACKAGE [--exact] [--caret] [--dev] [--peer] [-m] - [--all] + [--all] [--variant VARIANT] Adds specified package(s) to the dependencies of the current project (as @@ -126,6 +126,9 @@ Optional arguments: same version of the dependency. --all If specified, the dependency will be added to all projects. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. " `; @@ -347,6 +350,8 @@ Optional arguments: exports[`CommandLineHelp prints the help for each action: check 1`] = ` "usage: rush check [-h] [--json] [--verbose] [--subspace SUBSPACE_NAME] + [--variant VARIANT] + Checks each project's package.json files and ensures that all dependencies are of the same version throughout the repository. @@ -364,6 +369,9 @@ Optional arguments: within that subspace (ignoring other subspaces). This parameter is required when the \\"subspacesEnabled\\" setting is set to true in subspaces.json. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. " `; @@ -624,8 +632,8 @@ exports[`CommandLineHelp prints the help for each action: install 1`] = ` "usage: rush install [-h] [-p] [--bypass-policy] [--no-link] [--network-concurrency COUNT] [--debug-package-manager] [--max-install-attempts NUMBER] [--ignore-hooks] - [--offline] [-t PROJECT] [-T PROJECT] [-f PROJECT] - [-o PROJECT] [-i PROJECT] [-I PROJECT] + [--offline] [--variant VARIANT] [-t PROJECT] [-T PROJECT] + [-f PROJECT] [-o PROJECT] [-i PROJECT] [-I PROJECT] [--to-version-policy VERSION_POLICY_NAME] [--from-version-policy VERSION_POLICY_NAME] [--subspace SUBSPACE_NAME] [--check-only] @@ -675,6 +683,9 @@ Optional arguments: necessary NPM packages cannot be obtained from the local cache. For details, see the documentation for PNPM's \\"--offline\\" parameter. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. -t PROJECT, --to PROJECT Normally all projects in the monorepo will be processed; adding this parameter will instead select @@ -1141,7 +1152,7 @@ Optional arguments: `; exports[`CommandLineHelp prints the help for each action: remove 1`] = ` -"usage: rush remove [-h] [-s] -p PACKAGE [--all] +"usage: rush remove [-h] [-s] -p PACKAGE [--all] [--variant VARIANT] Removes specified package(s) from the dependencies of the current project (as determined by the current working directory) and then runs \\"rush update\\". @@ -1156,6 +1167,9 @@ Optional arguments: foo --package bar\\". --all If specified, the dependency will be removed from all projects that declare it. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. " `; @@ -1223,7 +1237,7 @@ exports[`CommandLineHelp prints the help for each action: update 1`] = ` "usage: rush update [-h] [-p] [--bypass-policy] [--no-link] [--network-concurrency COUNT] [--debug-package-manager] [--max-install-attempts NUMBER] [--ignore-hooks] - [--offline] [--full] [--recheck] + [--offline] [--variant VARIANT] [--full] [--recheck] The \\"rush update\\" command installs the dependencies described in your package. @@ -1268,6 +1282,9 @@ Optional arguments: necessary NPM packages cannot be obtained from the local cache. For details, see the documentation for PNPM's \\"--offline\\" parameter. + --variant VARIANT Run command using a variant installation + configuration. This parameter may alternatively be + specified via the RUSH_VARIANT environment variable. --full Normally \\"rush update\\" tries to preserve your existing installed versions and only makes the minimum updates needed to satisfy the package.json @@ -1324,6 +1341,8 @@ Optional arguments: exports[`CommandLineHelp prints the help for each action: upgrade-interactive 1`] = ` "usage: rush upgrade-interactive [-h] [--make-consistent] [-s] + [--variant VARIANT] + Provide an interactive way to upgrade your dependencies. Running the command will open an interactive prompt that will ask you which projects and which @@ -1341,6 +1360,9 @@ Optional arguments: upgrade dependencies from other projects. -s, --skip-update If specified, the \\"rush update\\" command will not be run after updating the package.json files. + --variant VARIANT Run command using a variant installation configuration. + This parameter may alternatively be specified via the + RUSH_VARIANT environment variable. " `; diff --git a/libraries/rush-lib/src/logic/DependencyAnalyzer.ts b/libraries/rush-lib/src/logic/DependencyAnalyzer.ts index 3ffba41fcb9..531cd46b69f 100644 --- a/libraries/rush-lib/src/logic/DependencyAnalyzer.ts +++ b/libraries/rush-lib/src/logic/DependencyAnalyzer.ts @@ -33,7 +33,7 @@ export class DependencyAnalyzer { | undefined; private _rushConfiguration: RushConfiguration; - private _analysisBySubspace: WeakMap | undefined; + private _analysisByVariantBySubspace: Map> | undefined; private constructor(rushConfiguration: RushConfiguration) { this._rushConfiguration = rushConfiguration; @@ -54,20 +54,36 @@ export class DependencyAnalyzer { return analyzer; } - public getAnalysis(subspace?: Subspace, addAction?: boolean): IDependencyAnalysis { - if (!this._analysisBySubspace) { - this._analysisBySubspace = new WeakMap(); + public getAnalysis( + subspace: Subspace | undefined, + variant: string | undefined, + addAction: boolean + ): IDependencyAnalysis { + // Use an empty string as the key when no variant provided. Anything else would possibly conflict + // with a variant created by the user + const variantKey: string = variant || ''; + + if (!this._analysisByVariantBySubspace) { + this._analysisByVariantBySubspace = new Map(); } const subspaceToAnalyze: Subspace = subspace || this._rushConfiguration.defaultSubspace; - if (!this._analysisBySubspace.has(subspaceToAnalyze)) { - this._analysisBySubspace.set( - subspaceToAnalyze, - this._getAnalysisInternal(subspace || this._rushConfiguration.defaultSubspace, addAction) - ); + let analysisForVariant: WeakMap | undefined = + this._analysisByVariantBySubspace.get(variantKey); + + if (!analysisForVariant) { + analysisForVariant = new WeakMap(); + this._analysisByVariantBySubspace.set(variantKey, analysisForVariant); + } + + let analysisForSubspace: IDependencyAnalysis | undefined = analysisForVariant.get(subspaceToAnalyze); + if (!analysisForSubspace) { + analysisForSubspace = this._getAnalysisInternal(subspaceToAnalyze, variant, addAction); + + analysisForVariant.set(subspaceToAnalyze, analysisForSubspace); } - return this._analysisBySubspace.get(subspaceToAnalyze) as IDependencyAnalysis; + return analysisForSubspace; } /** @@ -76,8 +92,12 @@ export class DependencyAnalyzer { * @remarks * The result of this function is not cached. */ - private _getAnalysisInternal(subspace: Subspace, addAction?: boolean): IDependencyAnalysis { - const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(); + private _getAnalysisInternal( + subspace: Subspace, + variant: string | undefined, + addAction: boolean + ): IDependencyAnalysis { + const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(variant); const allVersionsByPackageName: Map> = new Map(); const allowedAlternativeVersions: Map< string, diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts index dac1787ed4d..7f24813d06b 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdater.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdater.ts @@ -53,6 +53,10 @@ export interface IPackageJsonUpdaterRushUpgradeOptions { * If specified, "rush update" will be run in debug mode. */ debugInstall: boolean; + /** + * The variant to consider when performing installations and validating shrinkwrap updates. + */ + variant: string | undefined; } /** @@ -112,7 +116,7 @@ export class PackageJsonUpdater { * "rush upgrade-interactive". */ public async doRushUpgradeAsync(options: IPackageJsonUpdaterRushUpgradeOptions): Promise { - const { projects, packagesToAdd, updateOtherPackages, skipUpdate, debugInstall } = options; + const { projects, packagesToAdd, updateOtherPackages, skipUpdate, debugInstall, variant } = options; const { DependencyAnalyzer } = await import( /* webpackChunkName: 'DependencyAnalyzer' */ './DependencyAnalyzer' @@ -124,7 +128,7 @@ export class PackageJsonUpdater { allVersionsByPackageName, implicitlyPreferredVersionByPackageName, commonVersionsConfiguration - }: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(); + }: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(undefined, variant, false); const dependenciesToUpdate: Record = {}; const devDependenciesToUpdate: Record = {}; @@ -216,7 +220,8 @@ export class PackageJsonUpdater { if (updateOtherPackages) { const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches( - this._rushConfiguration + this._rushConfiguration, + options ); for (const update of this._getUpdates(mismatchFinder, allDependenciesToUpdate)) { this.updateProject(update); @@ -236,10 +241,10 @@ export class PackageJsonUpdater { options.projects ); for (const subspace of subspaceSet) { - await this._doUpdateAsync(debugInstall, subspace); + await this._doUpdateAsync(debugInstall, subspace, variant); } } else { - await this._doUpdateAsync(debugInstall, this._rushConfiguration.defaultSubspace); + await this._doUpdateAsync(debugInstall, this._rushConfiguration.defaultSubspace, variant); } } } @@ -253,7 +258,7 @@ export class PackageJsonUpdater { } else { throw new Error('only accept "rush add" or "rush remove"'); } - const { skipUpdate, debugInstall } = options; + const { skipUpdate, debugInstall, variant } = options; for (const { project } of allPackageUpdates) { if (project.saveIfModified()) { this._terminal.writeLine(Colorize.green('Wrote'), project.filePath); @@ -266,15 +271,19 @@ export class PackageJsonUpdater { options.projects ); for (const subspace of subspaceSet) { - await this._doUpdateAsync(debugInstall, subspace); + await this._doUpdateAsync(debugInstall, subspace, variant); } } else { - await this._doUpdateAsync(debugInstall, this._rushConfiguration.defaultSubspace); + await this._doUpdateAsync(debugInstall, this._rushConfiguration.defaultSubspace, variant); } } } - private async _doUpdateAsync(debugInstall: boolean, subspace: Subspace): Promise { + private async _doUpdateAsync( + debugInstall: boolean, + subspace: Subspace, + variant: string | undefined + ): Promise { this._terminal.writeLine(); this._terminal.writeLine(Colorize.green('Running "rush update"')); this._terminal.writeLine(); @@ -290,6 +299,7 @@ export class PackageJsonUpdater { networkConcurrency: undefined, offline: false, collectLogFile: false, + variant, maxInstallAttempts: RushConstants.defaultMaxInstallAttempts, pnpmFilterArgumentValues: [], selectedProjects: new Set(this._rushConfiguration.projects), @@ -342,7 +352,8 @@ export class PackageJsonUpdater { dependencyAnalyzer: DependencyAnalyzer, options: IPackageJsonUpdaterRushAddOptions ): Promise { - const { projects, packagesToUpdate, devDependency, peerDependency, updateOtherPackages } = options; + const { projects, packagesToUpdate, devDependency, peerDependency, updateOtherPackages, variant } = + options; // Get projects for this subspace const subspaceProjects: RushConfigurationProject[] = projects.filter( @@ -353,7 +364,7 @@ export class PackageJsonUpdater { allVersionsByPackageName, implicitlyPreferredVersionByPackageName, commonVersionsConfiguration - }: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(subspace, options.actionName === 'add'); + }: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(subspace, variant, options.actionName === 'add'); this._terminal.writeLine(); const dependenciesToAddOrUpdate: Record = {}; @@ -418,7 +429,8 @@ export class PackageJsonUpdater { const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches( this._rushConfiguration, { - subspace + subspace, + variant } ); otherPackageUpdates = this._getUpdates(mismatchFinder, Object.entries(dependenciesToAddOrUpdate)); diff --git a/libraries/rush-lib/src/logic/PackageJsonUpdaterTypes.ts b/libraries/rush-lib/src/logic/PackageJsonUpdaterTypes.ts index cc90cebd26c..bdc9ee6b285 100644 --- a/libraries/rush-lib/src/logic/PackageJsonUpdaterTypes.ts +++ b/libraries/rush-lib/src/logic/PackageJsonUpdaterTypes.ts @@ -53,6 +53,10 @@ export interface IPackageJsonUpdaterRushBaseUpdateOptions { * actionName */ actionName: string; + /** + * The variant to consider when performing installations and validating shrinkwrap updates. + */ + variant: string | undefined | undefined; } /** diff --git a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts index bc2c1408ce0..9b068940425 100644 --- a/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/libraries/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -31,6 +31,7 @@ export interface IGetChangedProjectsOptions { targetBranchName: string; terminal: ITerminal; shouldFetch?: boolean; + variant?: string; /** * If set to `true`, consider a project's external dependency installation layout as defined in the @@ -217,7 +218,8 @@ export class ProjectChangeAnalyzer { ): Promise> { const { _rushConfiguration: rushConfiguration } = this; - const { targetBranchName, terminal, includeExternalDependencies, enableFiltering, shouldFetch } = options; + const { targetBranchName, terminal, includeExternalDependencies, enableFiltering, shouldFetch, variant } = + options; const gitPath: string = this._git.getGitPathOrThrow(); const repoRoot: string = getRepoRoot(rushConfiguration.rushJsonFolder); @@ -236,10 +238,15 @@ export class ProjectChangeAnalyzer { // Even though changing the installed version of a nested dependency merits a change file, // ignore lockfile changes for `rush change` for the moment - const fullShrinkwrapPath: string = rushConfiguration.getCommittedShrinkwrapFilename(); + const variantToUse: string | undefined = + variant ?? (await this._rushConfiguration.getCurrentlyInstalledVariantAsync()); + const fullShrinkwrapPath: string = + rushConfiguration.defaultSubspace.getCommittedShrinkwrapFilePath(variantToUse); - const shrinkwrapFile: string = Path.convertToSlashes(path.relative(repoRoot, fullShrinkwrapPath)); - const shrinkwrapStatus: IFileDiffStatus | undefined = repoChanges.get(shrinkwrapFile); + const relativeShrinkwrapFilePath: string = Path.convertToSlashes( + path.relative(repoRoot, fullShrinkwrapPath) + ); + const shrinkwrapStatus: IFileDiffStatus | undefined = repoChanges.get(relativeShrinkwrapFilePath); if (shrinkwrapStatus) { if (shrinkwrapStatus.status !== 'M') { @@ -259,7 +266,7 @@ export class ProjectChangeAnalyzer { const oldShrinkwrapText: string = await this._git.getBlobContentAsync({ // : syntax: https://git-scm.com/docs/gitrevisions - blobSpec: `${mergeCommit}:${shrinkwrapFile}`, + blobSpec: `${mergeCommit}:${relativeShrinkwrapFilePath}`, repositoryRoot: repoRoot }); const oldShrinkWrap: PnpmShrinkwrapFile = PnpmShrinkwrapFile.loadFromString(oldShrinkwrapText); @@ -354,9 +361,14 @@ export class ProjectChangeAnalyzer { // Currently, only pnpm handles project shrinkwraps if (this._rushConfiguration.packageManager !== 'pnpm') { + const currentVariant: string | undefined = + await this._rushConfiguration.getCurrentlyInstalledVariantAsync(); // Add the shrinkwrap file to every project's dependencies const shrinkwrapFile: string = Path.convertToSlashes( - path.relative(rootDir, this._rushConfiguration.getCommittedShrinkwrapFilename()) + path.relative( + rootDir, + this._rushConfiguration.defaultSubspace.getCommittedShrinkwrapFilePath(currentVariant) + ) ); const shrinkwrapHash: string | undefined = repoDeps.get(shrinkwrapFile); diff --git a/libraries/rush-lib/src/logic/RepoStateFile.ts b/libraries/rush-lib/src/logic/RepoStateFile.ts index d0efdee51d8..3bdff9898ed 100644 --- a/libraries/rush-lib/src/logic/RepoStateFile.ts +++ b/libraries/rush-lib/src/logic/RepoStateFile.ts @@ -151,7 +151,11 @@ export class RepoStateFile { * * @returns true if the file was modified, otherwise false. */ - public refreshState(rushConfiguration: RushConfiguration, subspace: Subspace | undefined): boolean { + public refreshState( + rushConfiguration: RushConfiguration, + subspace: Subspace | undefined, + variant?: string + ): boolean { if (subspace === undefined) { subspace = rushConfiguration.defaultSubspace; } @@ -163,7 +167,7 @@ export class RepoStateFile { rushConfiguration.pnpmOptions.preventManualShrinkwrapChanges; if (preventShrinkwrapChanges) { const pnpmShrinkwrapFile: PnpmShrinkwrapFile | undefined = PnpmShrinkwrapFile.loadFromFile( - subspace.getCommittedShrinkwrapFilename() + subspace.getCommittedShrinkwrapFilePath(variant) ); if (pnpmShrinkwrapFile) { @@ -185,7 +189,7 @@ export class RepoStateFile { const useWorkspaces: boolean = rushConfiguration.pnpmOptions && rushConfiguration.pnpmOptions.useWorkspaces; if (useWorkspaces) { - const commonVersions: CommonVersionsConfiguration = subspace.getCommonVersions(); + const commonVersions: CommonVersionsConfiguration = subspace.getCommonVersions(variant); const preferredVersionsHash: string = commonVersions.getPreferredVersionsHash(); if (this._preferredVersionsHash !== preferredVersionsHash) { this._preferredVersionsHash = preferredVersionsHash; @@ -198,7 +202,7 @@ export class RepoStateFile { if (rushConfiguration.packageManager === 'pnpm' && rushConfiguration.subspacesFeatureEnabled) { const packageJsonInjectedDependenciesHash: string | undefined = - subspace.getPackageJsonInjectedDependenciesHash(); + subspace.getPackageJsonInjectedDependenciesHash(variant); // packageJsonInjectedDependenciesHash is undefined, means there is no injected dependencies for that subspace // so we don't need to track the hash value for that subspace diff --git a/libraries/rush-lib/src/logic/RushConstants.ts b/libraries/rush-lib/src/logic/RushConstants.ts index ad0099d7cc1..ec09faced21 100644 --- a/libraries/rush-lib/src/logic/RushConstants.ts +++ b/libraries/rush-lib/src/logic/RushConstants.ts @@ -52,6 +52,13 @@ export class RushConstants { */ public static readonly rushTempNpmScope: '@rush-temp' = '@rush-temp'; + /** + * The folder name ("variants") under which named variant configurations for + * alternate dependency sets may be found. + * Example: `C:\MyRepo\common\config\rush\variants` + */ + public static readonly rushVariantsFolderName: 'variants' = 'variants'; + /** * The folder name ("temp") under the common folder, or under the .rush folder in each project's directory where * temporary files will be stored. @@ -335,4 +342,9 @@ export class RushConstants { * The filename for the machine-generated file that tracks state for Rush alerts. */ public static readonly rushAlertsStateFilename: 'rush-alerts-state.json' = 'rush-alerts-state.json'; + + /** + * The filename for the file that tracks which variant is currently installed. + */ + public static readonly currentVariantsFilename: 'current-variants.json' = 'current-variants.json'; } diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts index a7f25110613..e42e37157f6 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManager.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManager.ts @@ -40,7 +40,7 @@ import { } from '../../api/LastInstallFlag'; import type { PnpmPackageManager } from '../../api/packageManager/PnpmPackageManager'; import type { PurgeManager } from '../PurgeManager'; -import type { RushConfiguration } from '../../api/RushConfiguration'; +import type { ICurrentVariantJson, RushConfiguration } from '../../api/RushConfiguration'; import { Rush } from '../../api/Rush'; import type { RushGlobalFolder } from '../../api/RushGlobalFolder'; import { RushConstants } from '../RushConstants'; @@ -114,7 +114,7 @@ export abstract class BaseInstallManager { } public async doInstallAsync(): Promise { - const { allowShrinkwrapUpdates, selectedProjects, pnpmFilterArgumentValues, resolutionOnly } = + const { allowShrinkwrapUpdates, selectedProjects, pnpmFilterArgumentValues, resolutionOnly, variant } = this.options; const isFilteredInstall: boolean = pnpmFilterArgumentValues.length > 0; const useWorkspaces: boolean = @@ -156,10 +156,8 @@ export abstract class BaseInstallManager { .experimentsConfiguration.configuration.generateProjectImpactGraphDuringRushUpdate ? new ProjectImpactGraphGenerator(this._terminal, this.rushConfiguration) : undefined; - const { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate } = await this.prepareAsync( - subspace, - projectImpactGraphGenerator - ); + const { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate, variantIsUpToDate } = + await this.prepareAsync(subspace, variant, projectImpactGraphGenerator); if (this.options.checkOnly) { return; @@ -197,17 +195,18 @@ export abstract class BaseInstallManager { })); // Allow us to defer the file read until we need it - const canSkipInstall: () => boolean = () => { + const canSkipInstallAsync: () => Promise = async () => { // Based on timestamps, can we skip this install entirely? - const outputStats: FileSystemStats = FileSystem.getStatistics(commonTempInstallFlag.path); - return this.canSkipInstall(outputStats.mtime, subspace); + const outputStats: FileSystemStats = await FileSystem.getStatisticsAsync(commonTempInstallFlag.path); + return this.canSkipInstallAsync(outputStats.mtime, subspace, variant); }; if ( resolutionOnly || cleanInstall || + !variantIsUpToDate || !shrinkwrapIsUpToDate || - !canSkipInstall() || + !(await canSkipInstallAsync()) || !projectImpactGraphIsUpToDate ) { // eslint-disable-next-line no-console @@ -253,7 +252,7 @@ export abstract class BaseInstallManager { ]); if (this.options.allowShrinkwrapUpdates && !shrinkwrapIsUpToDate) { - const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilename(); + const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilePath(variant); const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( this.rushConfiguration.packageManager, this.rushConfiguration.packageManagerOptions, @@ -268,7 +267,7 @@ export abstract class BaseInstallManager { // Always update the state file if running "rush update" if (this.options.allowShrinkwrapUpdates) { - if (subspace.getRepoState().refreshState(this.rushConfiguration, subspace)) { + if (subspace.getRepoState().refreshState(this.rushConfiguration, subspace, variant)) { // eslint-disable-next-line no-console console.log( Colorize.yellow( @@ -379,7 +378,11 @@ export abstract class BaseInstallManager { protected abstract postInstallAsync(subspace: Subspace): Promise; - protected canSkipInstall(lastModifiedDate: Date, subspace: Subspace): boolean { + protected async canSkipInstallAsync( + lastModifiedDate: Date, + subspace: Subspace, + variant: string | undefined + ): Promise { // Based on timestamps, can we skip this install entirely? const potentiallyChangedFiles: string[] = []; @@ -391,38 +394,42 @@ export abstract class BaseInstallManager { // Additionally, if they pulled an updated shrinkwrap file from Git, // then we can't skip this install - potentiallyChangedFiles.push(subspace.getCommittedShrinkwrapFilename()); + potentiallyChangedFiles.push(subspace.getCommittedShrinkwrapFilePath(variant)); // Add common-versions.json file to the potentially changed files list. - potentiallyChangedFiles.push(subspace.getCommonVersionsFilePath()); + potentiallyChangedFiles.push(subspace.getCommonVersionsFilePath(variant)); // Add pnpm-config.json file to the potentially changed files list. - potentiallyChangedFiles.push(subspace.getPnpmConfigFilePath()); + potentiallyChangedFiles.push(subspace.getPnpmConfigFilePath(variant)); if (this.rushConfiguration.packageManager === 'pnpm') { // If the repo is using pnpmfile.js, consider that also - const pnpmFileFilename: string = subspace.getPnpmfilePath(); + const pnpmFileFilePath: string = subspace.getPnpmfilePath(variant); + const pnpmFileExists: boolean = await FileSystem.existsAsync(pnpmFileFilePath); - if (FileSystem.exists(pnpmFileFilename)) { - potentiallyChangedFiles.push(pnpmFileFilename); + if (pnpmFileExists) { + potentiallyChangedFiles.push(pnpmFileFilePath); } } - return Utilities.isFileTimestampCurrent(lastModifiedDate, potentiallyChangedFiles); + return await Utilities.isFileTimestampCurrentAsync(lastModifiedDate, potentiallyChangedFiles); } protected async prepareAsync( subspace: Subspace, + variant: string | undefined, projectImpactGraphGenerator: ProjectImpactGraphGenerator | undefined ): Promise<{ shrinkwrapIsUpToDate: boolean; npmrcHash: string | undefined; projectImpactGraphIsUpToDate: boolean; + variantIsUpToDate: boolean; }> { + const terminal: ITerminal = this._terminal; const { allowShrinkwrapUpdates } = this.options; // Check the policies - await PolicyValidator.validatePolicyAsync(this.rushConfiguration, subspace, this.options); + await PolicyValidator.validatePolicyAsync(this.rushConfiguration, subspace, variant, this.options); await this._installGitHooksAsync(); @@ -432,8 +439,7 @@ export abstract class BaseInstallManager { if (approvedPackagesChecker.approvedPackagesFilesAreOutOfDate) { approvedPackagesChecker.rewriteConfigFiles(); if (allowShrinkwrapUpdates) { - // eslint-disable-next-line no-console - console.log( + terminal.writeLine( Colorize.yellow( 'Approved package files have been updated. These updates should be committed to source control' ) @@ -454,7 +460,7 @@ export abstract class BaseInstallManager { // (If it's a full update, then we ignore the shrinkwrap from Git since it will be overwritten) if (!this.options.fullUpgrade) { - const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilename(); + const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilePath(variant); try { shrinkwrapFile = ShrinkwrapFileFactory.getShrinkwrapFile( this.rushConfiguration.packageManager, @@ -462,18 +468,14 @@ export abstract class BaseInstallManager { committedShrinkwrapFileName ); } catch (ex) { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( + terminal.writeLine(); + terminal.writeLine( `Unable to load the ${this.rushConfiguration.shrinkwrapFilePhrase}: ${(ex as Error).message}` ); if (!allowShrinkwrapUpdates) { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log(Colorize.red('You need to run "rush update" to fix this problem')); + terminal.writeLine(); + terminal.writeLine(Colorize.red('You need to run "rush update" to fix this problem')); throw new AlreadyReportedError(); } @@ -481,12 +483,46 @@ export abstract class BaseInstallManager { } } + // Write a file indicating which variant is being installed. + // This will be used by bulk scripts to determine the correct Shrinkwrap file to track. + const currentVariantJsonFilePath: string = this.rushConfiguration.currentVariantJsonFilePath; + const currentVariantJson: ICurrentVariantJson = { + variant: variant ?? null + }; + + // Determine if the variant is already current by updating current-variant.json. + // If nothing is written, the variant has not changed. + const variantIsUpToDate: boolean = !(await JsonFile.saveAsync( + currentVariantJson, + currentVariantJsonFilePath, + { + onlyIfChanged: true + } + )); + this.rushConfiguration._currentVariantJsonLoadingPromise = undefined; + + if (this.options.variant) { + terminal.writeLine(); + terminal.writeLine(Colorize.bold(`Using variant '${this.options.variant}' for installation.`)); + } else if (!variantIsUpToDate && !variant && this.rushConfiguration.variants.size > 0) { + terminal.writeLine(); + terminal.writeLine(Colorize.bold('Using the default variant for installation.')); + } + const extraNpmrcLines: string[] = []; if (this.rushConfiguration.subspacesFeatureEnabled) { // Look for a monorepo level .npmrc file const commonNpmrcPath: string = `${this.rushConfiguration.commonRushConfigFolder}/.npmrc`; - if (FileSystem.exists(commonNpmrcPath)) { - const commonNpmrcFileLines: string[] = FileSystem.readFile(commonNpmrcPath).toString().split('\n'); + let commonNpmrcFileLines: string[] | undefined; + try { + commonNpmrcFileLines = (await FileSystem.readFileAsync(commonNpmrcPath)).split('\n'); + } catch (e) { + if (!FileSystem.isNotExistError(e)) { + throw e; + } + } + + if (commonNpmrcFileLines) { extraNpmrcLines.push(...commonNpmrcFileLines); } @@ -569,13 +605,15 @@ export abstract class BaseInstallManager { await PnpmfileConfiguration.writeCommonTempPnpmfileShimAsync( this.rushConfiguration, subspace.getSubspaceTempFolderPath(), - subspace + subspace, + variant ); if (this.rushConfiguration.subspacesFeatureEnabled) { await SubspacePnpmfileConfiguration.writeCommonTempSubspaceGlobalPnpmfileAsync( this.rushConfiguration, - subspace + subspace, + variant ); } } @@ -589,14 +627,12 @@ export abstract class BaseInstallManager { ]); shrinkwrapIsUpToDate = shrinkwrapIsUpToDate && !this.options.recheckShrinkwrap; - this._syncTempShrinkwrap(subspace, shrinkwrapFile); + this._syncTempShrinkwrap(subspace, variant, shrinkwrapFile); // Write out the reported warnings if (shrinkwrapWarnings.length > 0) { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( + terminal.writeLine(); + terminal.writeLine( Colorize.yellow( PrintUtilities.wrapWords( `The ${this.rushConfiguration.shrinkwrapFilePhrase} contains the following issues:` @@ -605,18 +641,17 @@ export abstract class BaseInstallManager { ); for (const shrinkwrapWarning of shrinkwrapWarnings) { - // eslint-disable-next-line no-console - console.log(Colorize.yellow(' ' + shrinkwrapWarning)); + terminal.writeLine(Colorize.yellow(' ' + shrinkwrapWarning)); } - // eslint-disable-next-line no-console - console.log(); + + terminal.writeLine(); } let hasErrors: boolean = false; // Force update if the shrinkwrap is out of date if (!shrinkwrapIsUpToDate && !allowShrinkwrapUpdates) { - this._terminal.writeErrorLine(); - this._terminal.writeErrorLine( + terminal.writeErrorLine(); + terminal.writeErrorLine( `The ${this.rushConfiguration.shrinkwrapFilePhrase} is out of date. You need to run "rush update".` ); hasErrors = true; @@ -624,8 +659,8 @@ export abstract class BaseInstallManager { if (!projectImpactGraphIsUpToDate && !allowShrinkwrapUpdates) { hasErrors = true; - this._terminal.writeErrorLine(); - this._terminal.writeErrorLine( + terminal.writeErrorLine(); + terminal.writeErrorLine( Colorize.red( `The ${RushConstants.projectImpactGraphFilename} file is missing or out of date. You need to run "rush update".` ) @@ -636,7 +671,7 @@ export abstract class BaseInstallManager { throw new AlreadyReportedError(); } - return { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate }; + return { shrinkwrapIsUpToDate, npmrcHash, projectImpactGraphIsUpToDate, variantIsUpToDate }; } /** @@ -1070,8 +1105,12 @@ ${gitLfsHookHandling} return true; } - private _syncTempShrinkwrap(subspace: Subspace, shrinkwrapFile: BaseShrinkwrapFile | undefined): void { - const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilename(); + private _syncTempShrinkwrap( + subspace: Subspace, + variant: string | undefined, + shrinkwrapFile: BaseShrinkwrapFile | undefined + ): void { + const committedShrinkwrapFileName: string = subspace.getCommittedShrinkwrapFilePath(variant); if (shrinkwrapFile) { Utilities.syncFile(committedShrinkwrapFileName, subspace.getTempShrinkwrapFilename()); Utilities.syncFile(committedShrinkwrapFileName, subspace.getTempShrinkwrapPreinstallFilename()); diff --git a/libraries/rush-lib/src/logic/base/BaseInstallManagerTypes.ts b/libraries/rush-lib/src/logic/base/BaseInstallManagerTypes.ts index 044667e2e2c..47017a1da12 100644 --- a/libraries/rush-lib/src/logic/base/BaseInstallManagerTypes.ts +++ b/libraries/rush-lib/src/logic/base/BaseInstallManagerTypes.ts @@ -81,6 +81,11 @@ export interface IInstallManagerOptions { */ collectLogFile: boolean; + /** + * The variant to consider when performing installations and validating shrinkwrap updates. + */ + variant: string | undefined; + /** * Retry the install the specified number of times */ diff --git a/libraries/rush-lib/src/logic/base/BaseShrinkwrapFile.ts b/libraries/rush-lib/src/logic/base/BaseShrinkwrapFile.ts index 928d66116a1..bfedfdcd88e 100644 --- a/libraries/rush-lib/src/logic/base/BaseShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/base/BaseShrinkwrapFile.ts @@ -154,12 +154,14 @@ export abstract class BaseShrinkwrapFile { * a given package.json. Returns true if any dependencies are not aligned with the shrinkwrap. * * @param project - the Rush project that is being validated against the shrinkwrap + * @param variant - the variant that is being validated * * @virtual */ public abstract isWorkspaceProjectModifiedAsync( project: RushConfigurationProject, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise; /** @virtual */ diff --git a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts index 717b1e0cb1e..03b9c506d8d 100644 --- a/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/RushInstallManager.ts @@ -90,6 +90,8 @@ export class RushInstallManager extends BaseInstallManager { ): Promise<{ shrinkwrapIsUpToDate: boolean; shrinkwrapWarnings: string[] }> { const stopwatch: Stopwatch = Stopwatch.start(); + const { fullUpgrade, variant } = this.options; + // Example: "C:\MyRepo\common\temp\projects" const tempProjectsFolder: string = path.join( this.rushConfiguration.commonTempFolder, @@ -109,7 +111,7 @@ export class RushInstallManager extends BaseInstallManager { if (!shrinkwrapFile) { shrinkwrapIsUpToDate = false; - } else if (shrinkwrapFile.isWorkspaceCompatible && !this.options.fullUpgrade) { + } else if (shrinkwrapFile.isWorkspaceCompatible && !fullUpgrade) { // eslint-disable-next-line no-console console.log(); // eslint-disable-next-line no-console @@ -124,7 +126,7 @@ export class RushInstallManager extends BaseInstallManager { // dependency name --> version specifier const allExplicitPreferredVersions: Map = this.rushConfiguration.defaultSubspace - .getCommonVersions() + .getCommonVersions(variant) .getAllPreferredVersions(); if (shrinkwrapFile) { @@ -167,7 +169,7 @@ export class RushInstallManager extends BaseInstallManager { // dependency name --> version specifier const commonDependencies: Map = new Map([ ...allExplicitPreferredVersions, - ...this.rushConfiguration.getImplicitlyPreferredVersions(subspace) + ...this.rushConfiguration.getImplicitlyPreferredVersions(subspace, variant) ]); // To make the common/package.json file more readable, sort alphabetically @@ -438,8 +440,12 @@ export class RushInstallManager extends BaseInstallManager { * * @override */ - protected canSkipInstall(lastModifiedDate: Date, subspace: Subspace): boolean { - if (!super.canSkipInstall(lastModifiedDate, subspace)) { + protected async canSkipInstallAsync( + lastModifiedDate: Date, + subspace: Subspace, + variant: string | undefined + ): Promise { + if (!(await super.canSkipInstallAsync(lastModifiedDate, subspace, variant))) { return false; } @@ -454,7 +460,7 @@ export class RushInstallManager extends BaseInstallManager { }) ); - return Utilities.isFileTimestampCurrent(lastModifiedDate, potentiallyChangedFiles); + return Utilities.isFileTimestampCurrentAsync(lastModifiedDate, potentiallyChangedFiles); } /** diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 3ce4aa5f65c..8308aef00c8 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -90,6 +90,8 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); } + const { fullUpgrade, allowShrinkwrapUpdates, variant } = this.options; + // eslint-disable-next-line no-console console.log('\n' + Colorize.bold('Updating workspace files in ' + subspace.getSubspaceTempFolderPath())); @@ -102,7 +104,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { if (!shrinkwrapFile) { shrinkwrapIsUpToDate = false; } else { - if (!shrinkwrapFile.isWorkspaceCompatible && !this.options.fullUpgrade) { + if (!shrinkwrapFile.isWorkspaceCompatible && !fullUpgrade) { // eslint-disable-next-line no-console console.log(); // eslint-disable-next-line no-console @@ -141,7 +143,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); shrinkwrapIsUpToDate = false; } else { - const commonVersions: CommonVersionsConfiguration = subspace.getCommonVersions(); + const commonVersions: CommonVersionsConfiguration = subspace.getCommonVersions(variant); if (repoState.preferredVersionsHash !== commonVersions.getPreferredVersionsHash()) { shrinkwrapWarnings.push( `Preferred versions from ${RushConstants.commonVersionsFilename} have been modified.` @@ -152,7 +154,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { const stopwatch: Stopwatch = Stopwatch.start(); const packageJsonInjectedDependenciesHash: string | undefined = - subspace.getPackageJsonInjectedDependenciesHash(); + subspace.getPackageJsonInjectedDependenciesHash(variant); stopwatch.stop(); @@ -253,7 +255,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { throw new AlreadyReportedError(); } - if (!this.options.allowShrinkwrapUpdates) { + if (!allowShrinkwrapUpdates) { // eslint-disable-next-line no-console console.log(); // eslint-disable-next-line no-console @@ -269,7 +271,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { throw new AlreadyReportedError(); } - if (this.options.fullUpgrade) { + if (fullUpgrade) { // We will update to `workspace` notation. If the version specified is a range, then use the provided range. // Otherwise, use `workspace:*` to ensure we're always using the workspace package. const workspaceRange: string = @@ -312,7 +314,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { } // Now validate that the shrinkwrap file matches what is in the package.json - if (await shrinkwrapFile?.isWorkspaceProjectModifiedAsync(rushProject, subspace)) { + if (await shrinkwrapFile?.isWorkspaceProjectModifiedAsync(rushProject, subspace, variant)) { shrinkwrapWarnings.push( `Dependencies of project "${rushProject.packageName}" do not match the current shrinkwrap.` ); @@ -414,8 +416,12 @@ export class WorkspaceInstallManager extends BaseInstallManager { return packageExtensionsChecksum; } - protected canSkipInstall(lastModifiedDate: Date, subspace: Subspace): boolean { - if (!super.canSkipInstall(lastModifiedDate, subspace)) { + protected async canSkipInstallAsync( + lastModifiedDate: Date, + subspace: Subspace, + variant: string | undefined + ): Promise { + if (!(await super.canSkipInstallAsync(lastModifiedDate, subspace, variant))) { return false; } @@ -447,7 +453,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { // NOTE: If any of the potentiallyChangedFiles does not exist, then isFileTimestampCurrent() // returns false. - return Utilities.isFileTimestampCurrent(lastModifiedDate, potentiallyChangedFiles); + return Utilities.isFileTimestampCurrentAsync(lastModifiedDate, potentiallyChangedFiles); } /** diff --git a/libraries/rush-lib/src/logic/installManager/doBasicInstallAsync.ts b/libraries/rush-lib/src/logic/installManager/doBasicInstallAsync.ts index 952f85dd525..8c830471f5d 100644 --- a/libraries/rush-lib/src/logic/installManager/doBasicInstallAsync.ts +++ b/libraries/rush-lib/src/logic/installManager/doBasicInstallAsync.ts @@ -11,6 +11,7 @@ import { InstallManagerFactory } from '../InstallManagerFactory'; import { SetupChecks } from '../SetupChecks'; import { PurgeManager } from '../PurgeManager'; import { VersionMismatchFinder } from '../versionMismatch/VersionMismatchFinder'; +import type { Subspace } from '../../api/Subspace'; export interface IRunInstallOptions { afterInstallAsync?: IInstallManagerOptions['afterInstallAsync']; @@ -19,12 +20,26 @@ export interface IRunInstallOptions { rushGlobalFolder: RushGlobalFolder; isDebug: boolean; terminal: ITerminal; + variant: string | undefined; + subspace: Subspace; } export async function doBasicInstallAsync(options: IRunInstallOptions): Promise { - const { rushConfiguration, rushGlobalFolder, isDebug } = options; + const { + rushConfiguration, + rushGlobalFolder, + isDebug, + variant, + terminal, + beforeInstallAsync, + afterInstallAsync, + subspace + } = options; - VersionMismatchFinder.ensureConsistentVersions(rushConfiguration, options.terminal); + VersionMismatchFinder.ensureConsistentVersions(rushConfiguration, terminal, { + variant, + subspace: undefined + }); SetupChecks.validate(rushConfiguration); const purgeManager: typeof PurgeManager.prototype = new PurgeManager(rushConfiguration, rushGlobalFolder); @@ -47,10 +62,11 @@ export async function doBasicInstallAsync(options: IRunInstallOptions): Promise< selectedProjects: new Set(rushConfiguration.projects), maxInstallAttempts: 1, networkConcurrency: undefined, - subspace: rushConfiguration.defaultSubspace, - terminal: options.terminal, - afterInstallAsync: options.afterInstallAsync, - beforeInstallAsync: options.beforeInstallAsync + subspace, + terminal, + variant, + afterInstallAsync, + beforeInstallAsync } ); diff --git a/libraries/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts index 72568ada1bc..7c11ac6400d 100644 --- a/libraries/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/npm/NpmShrinkwrapFile.ts @@ -134,7 +134,8 @@ export class NpmShrinkwrapFile extends BaseShrinkwrapFile { /** @override */ public async isWorkspaceProjectModifiedAsync( project: RushConfigurationProject, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { throw new InternalError('Not implemented'); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts index 3860200b737..5801d1dc1cc 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmShrinkwrapFile.ts @@ -809,7 +809,8 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { /** @override */ public async isWorkspaceProjectModifiedAsync( project: RushConfigurationProject, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { const importerKey: string = this.getImporterKeyByPath( subspace.getSubspaceTempFolderPath(), @@ -828,7 +829,8 @@ export class PnpmShrinkwrapFile extends BaseShrinkwrapFile { if (!this._pnpmfileConfiguration) { this._pnpmfileConfiguration = await PnpmfileConfiguration.initializeAsync( project.rushConfiguration, - subspace + subspace, + variant ); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmfileConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmfileConfiguration.ts index 6dece515a8a..9b89c94ced0 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmfileConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmfileConfiguration.ts @@ -29,7 +29,8 @@ export class PnpmfileConfiguration { public static async initializeAsync( rushConfiguration: RushConfiguration, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { if (rushConfiguration.packageManager !== 'pnpm') { throw new Error( @@ -42,7 +43,8 @@ export class PnpmfileConfiguration { log: (message: string) => {}, pnpmfileShimSettings: await PnpmfileConfiguration._getPnpmfileShimSettingsAsync( rushConfiguration, - subspace + subspace, + variant ) }; @@ -52,7 +54,8 @@ export class PnpmfileConfiguration { public static async writeCommonTempPnpmfileShimAsync( rushConfiguration: RushConfiguration, targetDir: string, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { if (rushConfiguration.packageManager !== 'pnpm') { throw new Error( @@ -72,7 +75,7 @@ export class PnpmfileConfiguration { }); const pnpmfileShimSettings: IPnpmfileShimSettings = - await PnpmfileConfiguration._getPnpmfileShimSettingsAsync(rushConfiguration, subspace); + await PnpmfileConfiguration._getPnpmfileShimSettingsAsync(rushConfiguration, subspace, variant); // Write the settings file used by the shim await JsonFile.saveAsync(pnpmfileShimSettings, path.join(targetDir, 'pnpmfileSettings.json'), { @@ -82,7 +85,8 @@ export class PnpmfileConfiguration { private static async _getPnpmfileShimSettingsAsync( rushConfiguration: RushConfiguration, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { let allPreferredVersions: { [dependencyName: string]: string } = {}; let allowedAlternativeVersions: { [dependencyName: string]: readonly string[] } = {}; @@ -90,11 +94,11 @@ export class PnpmfileConfiguration { // Only workspaces shims in the common versions using pnpmfile if ((rushConfiguration.packageManagerOptions as PnpmOptionsConfiguration).useWorkspaces) { - const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(); + const commonVersionsConfiguration: CommonVersionsConfiguration = subspace.getCommonVersions(variant); const preferredVersions: Map = new Map(); MapExtensions.mergeFromMap( preferredVersions, - rushConfiguration.getImplicitlyPreferredVersions(subspace) + rushConfiguration.getImplicitlyPreferredVersions(subspace, variant) ); for (const [name, version] of commonVersionsConfiguration.getAllPreferredVersions()) { // Use the most restrictive version range available @@ -120,7 +124,7 @@ export class PnpmfileConfiguration { }; // Use the provided path if available. Otherwise, use the default path. - const userPnpmfilePath: string | undefined = subspace.getPnpmfilePath(); + const userPnpmfilePath: string | undefined = subspace.getPnpmfilePath(variant); if (userPnpmfilePath && FileSystem.exists(userPnpmfilePath)) { settings.userPnpmfilePath = userPnpmfilePath; } diff --git a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts index db11e43ac9e..327f1787de7 100644 --- a/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/SubspacePnpmfileConfiguration.ts @@ -25,7 +25,8 @@ export class SubspacePnpmfileConfiguration { */ public static async writeCommonTempSubspaceGlobalPnpmfileAsync( rushConfiguration: RushConfiguration, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): Promise { if (rushConfiguration.packageManager !== 'pnpm') { throw new Error( @@ -43,7 +44,7 @@ export class SubspacePnpmfileConfiguration { }); const subspaceGlobalPnpmfileShimSettings: ISubspacePnpmfileShimSettings = - SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings(rushConfiguration, subspace); + SubspacePnpmfileConfiguration.getSubspacePnpmfileShimSettings(rushConfiguration, subspace, variant); // Write the settings file used by the shim await JsonFile.saveAsync( @@ -57,7 +58,8 @@ export class SubspacePnpmfileConfiguration { public static getSubspacePnpmfileShimSettings( rushConfiguration: RushConfiguration, - subspace: Subspace + subspace: Subspace, + variant: string | undefined ): ISubspacePnpmfileShimSettings { const workspaceProjects: Record = {}; const subspaceProjects: Record = {}; @@ -85,7 +87,7 @@ export class SubspacePnpmfileConfiguration { // common/config/subspaces//.pnpmfile.cjs const userPnpmfilePath: string = path.join( - subspace.getSubspaceConfigFolderPath(), + subspace.getVariantDependentSubspaceConfigFolderPath(variant), (rushConfiguration.packageManagerWrapper as PnpmPackageManager).pnpmfileFilename ); if (FileSystem.exists(userPnpmfilePath)) { diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts index b433514c8ce..91f787e57b5 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmShrinkwrapFile.test.ts @@ -136,7 +136,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(false); }); @@ -149,7 +150,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(true); }); @@ -162,7 +164,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(false); }); @@ -177,7 +180,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(false); }); @@ -190,7 +194,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(true); }); @@ -203,7 +208,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(false); }); @@ -216,7 +222,8 @@ describe(PnpmShrinkwrapFile.name, () => { await expect( pnpmShrinkwrapFile.isWorkspaceProjectModifiedAsync( project, - project.rushConfiguration.defaultSubspace + project.rushConfiguration.defaultSubspace, + undefined ) ).resolves.toBe(false); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmfileConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmfileConfiguration.test.ts index b1875dbde8f..23dafd9caf1 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmfileConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmfileConfiguration.test.ts @@ -16,7 +16,8 @@ describe(PnpmfileConfiguration.name, () => { await PnpmfileConfiguration.writeCommonTempPnpmfileShimAsync( rushConfiguration, subspace.getSubspaceTempFolderPath(), - subspace + subspace, + undefined ); }); diff --git a/libraries/rush-lib/src/logic/policy/PolicyValidator.ts b/libraries/rush-lib/src/logic/policy/PolicyValidator.ts index a035a2ac802..b54e31d1295 100644 --- a/libraries/rush-lib/src/logic/policy/PolicyValidator.ts +++ b/libraries/rush-lib/src/logic/policy/PolicyValidator.ts @@ -16,6 +16,7 @@ export interface IPolicyValidatorOptions { export async function validatePolicyAsync( rushConfiguration: RushConfiguration, subspace: Subspace, + variant: string | undefined, options: IPolicyValidatorOptions ): Promise { if (!options.bypassPolicy) { @@ -24,7 +25,7 @@ export async function validatePolicyAsync( if (!options.allowShrinkwrapUpdates) { // Don't validate the shrinkwrap if updates are allowed, as it's likely to change // It also may have merge conflict markers, which PNPM can gracefully handle, but the validator cannot - ShrinkwrapFilePolicy.validate(rushConfiguration, subspace, options); + ShrinkwrapFilePolicy.validate(rushConfiguration, subspace, variant, options); } } } diff --git a/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts b/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts index 40788b8b00d..f774eae072b 100644 --- a/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts +++ b/libraries/rush-lib/src/logic/policy/ShrinkwrapFilePolicy.ts @@ -18,6 +18,7 @@ export interface IShrinkwrapFilePolicyValidatorOptions extends IPolicyValidatorO export function validate( rushConfiguration: RushConfiguration, subspace: Subspace, + variant: string | undefined, options: IPolicyValidatorOptions ): void { // eslint-disable-next-line no-console @@ -25,7 +26,7 @@ export function validate( const shrinkwrapFile: BaseShrinkwrapFile | undefined = ShrinkwrapFileFactory.getShrinkwrapFile( rushConfiguration.packageManager, rushConfiguration.packageManagerOptions, - subspace.getCommittedShrinkwrapFilename() + subspace.getCommittedShrinkwrapFilePath(variant) ); if (!shrinkwrapFile) { diff --git a/libraries/rush-lib/src/logic/test/DependencyAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/DependencyAnalyzer.test.ts index 392e3819221..04f192fef8a 100644 --- a/libraries/rush-lib/src/logic/test/DependencyAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/DependencyAnalyzer.test.ts @@ -10,7 +10,7 @@ describe(DependencyAnalyzer.name, () => { `${__dirname}/DependencyAnalyzerTestRepos/${repoName}/rush.json` ); const dependencyAnalyzer: DependencyAnalyzer = DependencyAnalyzer.forRushConfiguration(rushConfiguration); - const analysis: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(); + const analysis: IDependencyAnalysis = dependencyAnalyzer.getAnalysis(undefined, undefined, false); return analysis; } diff --git a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index 1c328acc28b..473b81c5444 100644 --- a/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/libraries/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -29,8 +29,10 @@ describe(ProjectChangeAnalyzer.name, () => { commonRushConfigFolder: '', projects, rushJsonFolder: '', - getCommittedShrinkwrapFilename(): string { - return 'common/config/rush/pnpm-lock.yaml'; + defaultSubspace: { + getCommittedShrinkwrapFilePath(variant: string | undefined): string { + return 'common/config/rush/pnpm-lock.yaml'; + } }, getProjectLookupForRoot(root: string): LookupByPath { const lookup: LookupByPath = new LookupByPath(); @@ -41,7 +43,8 @@ describe(ProjectChangeAnalyzer.name, () => { }, getProjectByName(name: string): RushConfigurationProject | undefined { return projects.find((project) => project.packageName === name); - } + }, + getCurrentlyInstalledVariantAsync: () => Promise.resolve(undefined) } as RushConfiguration; const subject: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(rushConfiguration); diff --git a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinder.ts b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinder.ts index 5fb83455965..567f1678f95 100644 --- a/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinder.ts +++ b/libraries/rush-lib/src/logic/versionMismatch/VersionMismatchFinder.ts @@ -16,13 +16,13 @@ import type { Subspace } from '../../api/Subspace'; const TRUNCATE_AFTER_PACKAGE_NAME_COUNT: number = 5; export interface IVersionMismatchFinderOptions { - subspace: Subspace; + subspace?: Subspace; + variant: string | undefined; } export interface IVersionMismatchFinderRushCheckOptions extends IVersionMismatchFinderOptions { printAsJson?: boolean | undefined; truncateLongPackageNameLists?: boolean | undefined; - subspace: Subspace; } export interface IVersionMismatchFinderEnsureConsistentVersionsOptions @@ -69,12 +69,20 @@ export class VersionMismatchFinder { public static rushCheck( rushConfiguration: RushConfiguration, terminal: ITerminal, - options: IVersionMismatchFinderRushCheckOptions = { - subspace: rushConfiguration.defaultSubspace - } + options?: IVersionMismatchFinderRushCheckOptions ): void { + const { + variant, + subspace = rushConfiguration.defaultSubspace, + printAsJson, + truncateLongPackageNameLists + } = options ?? {}; + VersionMismatchFinder._checkForInconsistentVersions(rushConfiguration, { - ...options, + variant, + subspace, + printAsJson, + truncateLongPackageNameLists, terminal, isRushCheckCommand: true }); @@ -83,12 +91,13 @@ export class VersionMismatchFinder { public static ensureConsistentVersions( rushConfiguration: RushConfiguration, terminal: ITerminal, - options: IVersionMismatchFinderEnsureConsistentVersionsOptions = { - subspace: rushConfiguration.defaultSubspace - } + options?: IVersionMismatchFinderEnsureConsistentVersionsOptions ): void { + const { variant, subspace = rushConfiguration.defaultSubspace } = options ?? {}; + VersionMismatchFinder._checkForInconsistentVersions(rushConfiguration, { - ...options, + subspace, + variant, terminal, isRushCheckCommand: false, truncateLongPackageNameLists: true @@ -101,11 +110,10 @@ export class VersionMismatchFinder { */ public static getMismatches( rushConfiguration: RushConfiguration, - options: IVersionMismatchFinderOptions = { - subspace: rushConfiguration.defaultSubspace - } + options?: IVersionMismatchFinderOptions ): VersionMismatchFinder { - const commonVersions: CommonVersionsConfiguration = options.subspace.getCommonVersions(); + const { subspace = rushConfiguration.defaultSubspace, variant } = options ?? {}; + const commonVersions: CommonVersionsConfiguration = subspace.getCommonVersions(variant); const projects: VersionMismatchFinderEntity[] = []; @@ -114,7 +122,7 @@ export class VersionMismatchFinder { projects.push(new VersionMismatchFinderCommonVersions(commonVersions)); // If subspace is specified, only go through projects in that subspace - for (const project of options.subspace.getProjects()) { + for (const project of subspace.getProjects()) { projects.push(new VersionMismatchFinderProject(project)); } @@ -126,36 +134,39 @@ export class VersionMismatchFinder { options: { isRushCheckCommand: boolean; subspace: Subspace; + variant: string | undefined; printAsJson?: boolean | undefined; terminal: ITerminal; truncateLongPackageNameLists?: boolean | undefined; } ): void { - if (options.subspace.shouldEnsureConsistentVersions || options.isRushCheckCommand) { + const { variant, isRushCheckCommand, printAsJson, subspace, truncateLongPackageNameLists, terminal } = + options; + if (subspace.shouldEnsureConsistentVersions(variant) || isRushCheckCommand) { const mismatchFinder: VersionMismatchFinder = VersionMismatchFinder.getMismatches( rushConfiguration, options ); - if (options.printAsJson) { + if (printAsJson) { mismatchFinder.printAsJson(); } else { - mismatchFinder.print(options.truncateLongPackageNameLists); + mismatchFinder.print(truncateLongPackageNameLists); if (mismatchFinder.numberOfMismatches > 0) { // eslint-disable-next-line no-console console.log( Colorize.red( `Found ${mismatchFinder.numberOfMismatches} mis-matching dependencies ${ - options.subspace?.subspaceName ? `in subspace: ${options.subspace?.subspaceName}` : '' + subspace?.subspaceName ? `in subspace: ${subspace?.subspaceName}` : '' }` ) ); rushConfiguration.customTipsConfiguration._showErrorTip( - options.terminal, + terminal, CustomTipId.TIP_RUSH_INCONSISTENT_VERSIONS ); - if (!options.isRushCheckCommand && options.truncateLongPackageNameLists) { + if (!isRushCheckCommand && truncateLongPackageNameLists) { // There isn't a --verbose flag in `rush install`/`rush update`, so a long list will always be truncated. // eslint-disable-next-line no-console console.log( @@ -165,7 +176,7 @@ export class VersionMismatchFinder { throw new AlreadyReportedError(); } else { - if (options.isRushCheckCommand) { + if (isRushCheckCommand) { // eslint-disable-next-line no-console console.log(Colorize.green(`Found no mis-matching dependencies!`)); } diff --git a/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts b/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts index a371be204f7..dd0e274526b 100644 --- a/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts +++ b/libraries/rush-lib/src/pluginFramework/RushLifeCycle.ts @@ -86,16 +86,16 @@ export class RushLifecycleHooks { /** * The hook to run between preparing the common/temp folder and invoking the package manager during "rush install" or "rush update". */ - public readonly beforeInstall: AsyncSeriesHook<[IGlobalCommand, Subspace]> = new AsyncSeriesHook< - [IGlobalCommand, Subspace] - >(['command', 'subspace'], 'beforeInstall'); + public readonly beforeInstall: AsyncSeriesHook< + [command: IGlobalCommand, subspace: Subspace, variant: string | undefined] + > = new AsyncSeriesHook(['command', 'subspace', 'variant'], 'beforeInstall'); /** * The hook to run after a successful install. */ - public readonly afterInstall: AsyncSeriesHook<[IRushCommand, Subspace]> = new AsyncSeriesHook< - [IRushCommand, Subspace] - >(['command', 'subspace'], 'afterInstall'); + public readonly afterInstall: AsyncSeriesHook< + [command: IRushCommand, subspace: Subspace, variant: string | undefined] + > = new AsyncSeriesHook(['command', 'subspace', 'variant'], 'afterInstall'); /** * A hook to allow plugins to hook custom logic to process telemetry data. diff --git a/libraries/rush-lib/src/schemas/rush.schema.json b/libraries/rush-lib/src/schemas/rush.schema.json index af9e2481548..dce5fcaae37 100644 --- a/libraries/rush-lib/src/schemas/rush.schema.json +++ b/libraries/rush-lib/src/schemas/rush.schema.json @@ -201,8 +201,22 @@ "additionalProperties": false }, "variants": { - "description": "DEPRECATED", - "type": "array" + "description": "Defines the list of installation variants for this repository. For more details about this feature, see this article: https://rushjs.io/pages/advanced/installation_variants/", + "type": "array", + "items": { + "type": "object", + "properties": { + "variantName": { + "description": "The name of the variant. Maps to common/rush/variants/{name} under the repository root.", + "type": "string" + }, + "description": { + "description": "", + "type": "string" + } + }, + "required": ["variantName", "description"] + } }, "repository": { "description": "The repository location", diff --git a/libraries/rush-lib/src/utilities/Utilities.ts b/libraries/rush-lib/src/utilities/Utilities.ts index b17dd397887..f4f8901b00c 100644 --- a/libraries/rush-lib/src/utilities/Utilities.ts +++ b/libraries/rush-lib/src/utilities/Utilities.ts @@ -14,7 +14,8 @@ import { type FileSystemStats, SubprocessTerminator, Executable, - type IWaitForExitResult + type IWaitForExitResult, + Async } from '@rushstack/node-core-library'; import type { RushConfiguration } from '../api/RushConfiguration'; @@ -290,19 +291,37 @@ export class Utilities { * NOTE: The filenames can also be paths for directories, in which case the directory * timestamp is compared. */ - public static isFileTimestampCurrent(dateToCompare: Date, inputFilenames: string[]): boolean { - for (const inputFilename of inputFilenames) { - if (!FileSystem.exists(inputFilename)) { - return false; - } + public static async isFileTimestampCurrentAsync( + dateToCompare: Date, + inputFilePaths: string[] + ): Promise { + let anyAreOutOfDate: boolean = false; + await Async.forEachAsync( + inputFilePaths, + async (filePath) => { + if (!anyAreOutOfDate) { + let inputStats: FileSystemStats | undefined; + try { + inputStats = await FileSystem.getStatisticsAsync(filePath); + } catch (e) { + if (FileSystem.isNotExistError(e)) { + // eslint-disable-next-line require-atomic-updates + anyAreOutOfDate = true; + } else { + throw e; + } + } - const inputStats: FileSystemStats = FileSystem.getStatistics(inputFilename); - if (dateToCompare < inputStats.mtime) { - return false; - } - } + if (inputStats && dateToCompare < inputStats.mtime) { + // eslint-disable-next-line require-atomic-updates + anyAreOutOfDate = true; + } + } + }, + { concurrency: 10 } + ); - return true; + return !anyAreOutOfDate; } public static async executeCommandAsync( diff --git a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts index de6ba6402a1..e7e347c7fb3 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/afterInstallAsync.ts @@ -45,18 +45,20 @@ function getPlatformInfo(): IPlatformInfo { * @param rushSession - The Rush Session * @param rushConfiguration - The Rush Configuration * @param subspace - The subspace that was just installed + * @param variant - The variant that was just installed * @param logger - The initialized logger */ export async function afterInstallAsync( rushSession: RushSession, rushConfiguration: RushConfiguration, subspace: Subspace, + variant: string | undefined, logger: ILogger ): Promise { const { terminal } = logger; const rushRoot: string = `${rushConfiguration.rushJsonFolder}/`; - const lockFilePath: string = subspace.getCommittedShrinkwrapFilename(); + const lockFilePath: string = subspace.getCommittedShrinkwrapFilePath(variant); const workspaceRoot: string = subspace.getSubspaceTempFolderPath(); const projectByImporterPath: LookupByPath = @@ -117,7 +119,9 @@ export async function afterInstallAsync( const filteredFiles: string[] = Object.keys(files).filter((file) => file.endsWith('/package.json')); if (filteredFiles.length > 0) { - const nestedPackageDirs: string[] = filteredFiles.map((x) => x.slice(0, /* -'/package.json'.length */ -13)); + const nestedPackageDirs: string[] = filteredFiles.map((x) => + x.slice(0, /* -'/package.json'.length */ -13) + ); if (nestedPackageDirs.length > 0) { // eslint-disable-next-line require-atomic-updates diff --git a/rush-plugins/rush-resolver-cache-plugin/src/index.ts b/rush-plugins/rush-resolver-cache-plugin/src/index.ts index b90fcae88c8..8f032b3e939 100644 --- a/rush-plugins/rush-resolver-cache-plugin/src/index.ts +++ b/rush-plugins/rush-resolver-cache-plugin/src/index.ts @@ -20,7 +20,7 @@ export default class RushResolverCachePlugin implements IRushPlugin { public apply(rushSession: RushSession, rushConfiguration: RushConfiguration): void { rushSession.hooks.afterInstall.tapPromise( this.pluginName, - async (command: IRushCommand, subspace: Subspace) => { + async (command: IRushCommand, subspace: Subspace, variant: string | undefined) => { const logger: ILogger = rushSession.getLogger('RushResolverCachePlugin'); if (rushConfiguration.packageManager !== 'pnpm') { @@ -47,7 +47,7 @@ export default class RushResolverCachePlugin implements IRushPlugin { './afterInstallAsync' ); - await afterInstallAsync(rushSession, rushConfiguration, subspace, logger); + await afterInstallAsync(rushSession, rushConfiguration, subspace, variant, logger); } ); }