diff --git a/common/changes/@microsoft/rush/will-subspace-patch-1_2024-01-31-03-00.json b/common/changes/@microsoft/rush/will-subspace-patch-1_2024-01-31-03-00.json new file mode 100644 index 00000000000..bd478971daa --- /dev/null +++ b/common/changes/@microsoft/rush/will-subspace-patch-1_2024-01-31-03-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "(EXPERIMENTAL) Enable filtered installs of subspaces and add a \"preventSelectingAllSubspaces\" setting", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ 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 dd228f7cd48..685de1635b4 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1375,13 +1375,15 @@ export class Subspace { export class SubspacesConfiguration { // @internal static _convertNameToEnvironmentVariable(subspaceName: string, splitWorkspaceCompatibility: boolean): string; - readonly enabled: boolean; static explainIfInvalidSubspaceName(subspaceName: string, splitWorkspaceCompatibility?: boolean): string | undefined; + readonly preventSelectingAllSubspaces: boolean; static requireValidSubspaceName(subspaceName: string, splitWorkspaceCompatibility?: boolean): void; readonly splitWorkspaceCompatibility: boolean; readonly subspaceJsonFilePath: string; readonly subspaceNames: ReadonlySet; // (undocumented) + readonly subspacesEnabled: boolean; + // (undocumented) static tryLoadFromConfigurationFile(subspaceJsonFilePath: string): SubspacesConfiguration | undefined; // (undocumented) static tryLoadFromDefaultLocation(rushConfiguration: RushConfiguration): SubspacesConfiguration | undefined; diff --git a/libraries/rush-lib/assets/rush-init/common/config/rush/subspaces.json b/libraries/rush-lib/assets/rush-init/common/config/rush/subspaces.json index 3526906a094..d3c3ae8c516 100644 --- a/libraries/rush-lib/assets/rush-init/common/config/rush/subspaces.json +++ b/libraries/rush-lib/assets/rush-init/common/config/rush/subspaces.json @@ -1,30 +1,35 @@ /** - * This is the main configuration file for Rush. + * This configuration file manages the experimental "subspaces" feature for Rush, + * which allows multiple PNPM lockfiles to be used in a single Rush workspace. * For full documentation, please see https://rushjs.io */ - { +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + /** - * Temporarily use the default schema until the subspaces schema is uploaded + * Set this flag to "true" to enable usage of subspaces. */ - "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json", + "subspacesEnabled": false, /** - * This specifies whether or not to use the "subspaces" feature in rush. This allows grouping of - * projects into discrete workspaces known as "subspaces". + * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype + * of this feature: https://github.com/microsoft/rushstack/pull/3481 + * It allows subspaces with only one project to store their config files in the project folder. */ - "enabled": false, + "splitWorkspaceCompatibility": false, /** - * (DEPRECATED) A property that allows subspace configuration files to be stored under project folders directly. - * Used for migrating from a split-workspace format where there may be a large number of individual lockfiles and - * starting subspaces. + * When a command such as "rush update" is invoked without the "--subspace" or "--to" + * parameters, Rush will install all subspaces. In a huge monorepo with numerous subspaces, + * this would be extremely slow. Set "preventSelectingAllSubspaces" to true to avoid this + * mistake by always requiring selection parameters for commands such as "rush update". */ - "splitWorkspaceCompatibility": false, + "preventSelectingAllSubspaces": false, - /** - * The next field is an object describing the available subspaces in this workspace. Each individual subspace - * must be defined in this array. - */ + /** + * The list of subspace names, which should be lowercase alphanumeric words separated by + * hyphens, for example "my-subspace". The corresponding config files will have paths + * such as "common/config/subspaces/my-subspace/package-lock.yaml". + */ "subspaceNames": [] - } diff --git a/libraries/rush-lib/src/api/RushConfiguration.ts b/libraries/rush-lib/src/api/RushConfiguration.ts index 0aaaa6e19a5..793c38a8c19 100644 --- a/libraries/rush-lib/src/api/RushConfiguration.ts +++ b/libraries/rush-lib/src/api/RushConfiguration.ts @@ -353,7 +353,7 @@ export class RushConfiguration { public readonly subspacesConfiguration: SubspacesConfiguration | undefined; /** - * Returns true if subspaces.json is present with "enabled=true". + * Returns true if subspaces.json is present with "subspacesEnabled=true". */ public readonly subspacesFeatureEnabled: boolean; @@ -633,7 +633,7 @@ export class RushConfiguration { // Try getting a subspace configuration this.subspacesConfiguration = SubspacesConfiguration.tryLoadFromDefaultLocation(this); - this.subspacesFeatureEnabled = !!this.subspacesConfiguration?.enabled; + this.subspacesFeatureEnabled = !!this.subspacesConfiguration?.subspacesEnabled; this._subspacesByName = new Map(); @@ -860,7 +860,7 @@ export class RushConfiguration { // Build the subspaces map const subspaceNames: string[] = []; let splitWorkspaceCompatibility: boolean = false; - if (this.subspacesConfiguration?.enabled) { + if (this.subspacesConfiguration?.subspacesEnabled) { splitWorkspaceCompatibility = this.subspacesConfiguration.splitWorkspaceCompatibility; subspaceNames.push(...this.subspacesConfiguration.subspaceNames); @@ -1310,7 +1310,10 @@ export class RushConfiguration { const subspace: Subspace | undefined = this._subspacesByName.get(subspaceName); if (!subspace) { // If the name is not even valid, that is more important information than if the subspace doesn't exist - SubspacesConfiguration.requireValidSubspaceName(subspaceName, this.subspacesFeatureEnabled); + SubspacesConfiguration.requireValidSubspaceName( + subspaceName, + this.subspacesConfiguration?.splitWorkspaceCompatibility + ); } return subspace; } diff --git a/libraries/rush-lib/src/api/SubspacesConfiguration.ts b/libraries/rush-lib/src/api/SubspacesConfiguration.ts index 73a4ba5308e..fd18f6fa5a4 100644 --- a/libraries/rush-lib/src/api/SubspacesConfiguration.ts +++ b/libraries/rush-lib/src/api/SubspacesConfiguration.ts @@ -21,8 +21,9 @@ export const SPLIT_WORKSPACE_SUBSPACE_NAME_REGEXP: RegExp = /^[a-z0-9][+_\-a-z0- * See subspace.schema.json for documentation. */ interface ISubspacesConfigurationJson { - enabled: boolean; + subspacesEnabled: boolean; splitWorkspaceCompatibility?: boolean; + preventSelectingAllSubspaces?: boolean; subspaceNames: string[]; } @@ -39,16 +40,21 @@ export class SubspacesConfiguration { */ public readonly subspaceJsonFilePath: string; - /** + /* * Determines if the subspace feature is enabled */ - public readonly enabled: boolean; + public readonly subspacesEnabled: boolean; /** * This determines if the subspaces feature supports adding configuration files under the project folder itself */ public readonly splitWorkspaceCompatibility: boolean; + /** + * This determines if selectors are required when installing and building + */ + public readonly preventSelectingAllSubspaces: boolean; + /** * A set of the available subspaces */ @@ -56,11 +62,12 @@ export class SubspacesConfiguration { private constructor(configuration: Readonly, subspaceJsonFilePath: string) { this.subspaceJsonFilePath = subspaceJsonFilePath; - this.enabled = configuration.enabled; + this.subspacesEnabled = configuration.subspacesEnabled; this.splitWorkspaceCompatibility = !!configuration.splitWorkspaceCompatibility; + this.preventSelectingAllSubspaces = !!configuration.preventSelectingAllSubspaces; const subspaceNames: Set = new Set(); for (const subspaceName of configuration.subspaceNames) { - SubspacesConfiguration.requireValidSubspaceName(subspaceName, this.enabled); + SubspacesConfiguration.requireValidSubspaceName(subspaceName, this.splitWorkspaceCompatibility); subspaceNames.add(subspaceName); } diff --git a/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts b/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts index 84d4938e0fd..712fd7033d2 100644 --- a/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts +++ b/libraries/rush-lib/src/cli/actions/BaseInstallAction.ts @@ -117,36 +117,21 @@ export abstract class BaseInstallAction extends BaseRushAction { protected getTargetSubspace(): Subspace { const parameterValue: string | undefined = this._subspaceParameter.value; - - if (this.rushConfiguration.subspacesFeatureEnabled) { - if (!parameterValue) { - // Temporarily ensure that a subspace is provided - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( - colors.red( - `The subspaces feature currently only supports installing for a specified set of subspace,` + - ` passed by the "--subspace" parameter or selected from targeted projects using any project selector.` - ) - ); - throw new AlreadyReportedError(); - } - return this.rushConfiguration.getSubspace(parameterValue); - } else { - if (parameterValue) { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( - colors.red( - `The "--subspace" parameter can only be passed if the "enabled" option is enabled in subspaces.json.` - ) - ); - throw new AlreadyReportedError(); - } - return this.rushConfiguration.defaultSubspace; + if (parameterValue && !this.rushConfiguration.subspacesFeatureEnabled) { + // eslint-disable-next-line no-console + console.log(); + // eslint-disable-next-line no-console + console.log( + colors.red( + `The "--subspace" parameter can only be passed if "subspacesEnabled" is set to true in subspaces.json.` + ) + ); + throw new AlreadyReportedError(); } + const selectedSubspace: Subspace | undefined = parameterValue + ? this.rushConfiguration.getSubspace(parameterValue) + : this.rushConfiguration.defaultSubspace; + return selectedSubspace; } protected async runAsync(): Promise { @@ -154,13 +139,35 @@ export abstract class BaseInstallAction extends BaseRushAction { // 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; - if (installManagerOptions.pnpmFilterArguments.length && this.rushConfiguration.subspacesFeatureEnabled) { - const selectedProjects: Set | undefined = - await this._selectionParameters?.getSelectedProjectsAsync(this._terminal); - if (selectedProjects) { - selectedSubspaces = this.rushConfiguration.getSubspacesForProjects(selectedProjects); + if (this.rushConfiguration.subspacesFeatureEnabled) { + if (installManagerOptions.pnpmFilterArguments.length) { + // Selecting a set of subspaces + const selectedProjects: Set | undefined = + await this._selectionParameters?.getSelectedProjectsAsync(this._terminal); + if (selectedProjects) { + selectedSubspaces = this.rushConfiguration.getSubspacesForProjects(selectedProjects); + } else { + throw new Error('The specified filter arguments resulted in no projects being selected.'); + } + } else if (this._subspaceParameter.value) { + // Selecting a single subspace + const selectedSubspace: Subspace = this.rushConfiguration.getSubspace(this._subspaceParameter.value); + selectedSubspaces = new Set([selectedSubspace]); } else { - throw new Error('The specified filter arguments resulted in no projects being selected.'); + // Selecting all subspaces if preventSelectingAllSubspaces is not enabled in subspaces.json + if (!this.rushConfiguration.subspacesConfiguration?.preventSelectingAllSubspaces) { + selectedSubspaces = new Set(this.rushConfiguration.subspaces); + } else { + // eslint-disable-next-line no-console + console.log(); + // eslint-disable-next-line no-console + console.log( + colors.red( + `The subspaces preventSelectingAllSubspaces configuration is enabled, which enforces installation for a specified set of subspace,` + + ` passed by the "--subspace" parameter or selected from targeted projects using any project selector.` + ) + ); + } } } @@ -172,12 +179,6 @@ export abstract class BaseInstallAction extends BaseRushAction { subspace }); } - } else if (this._subspaceParameter.value) { - const subspace: Subspace = this.rushConfiguration.getSubspace(this._subspaceParameter.value); - VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this._terminal, { - variant: this._variant.value, - subspace: subspace - }); } else { VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this._terminal, { variant: this._variant.value diff --git a/libraries/rush-lib/src/cli/actions/InitAction.ts b/libraries/rush-lib/src/cli/actions/InitAction.ts index d20a17313dc..557a6e60ee1 100644 --- a/libraries/rush-lib/src/cli/actions/InitAction.ts +++ b/libraries/rush-lib/src/cli/actions/InitAction.ts @@ -177,6 +177,7 @@ export class InitAction extends BaseConfiglessRushAction { 'common/config/rush/experiments.json', 'common/config/rush/pnpm-config.json', 'common/config/rush/rush-plugins.json', + 'common/config/rush/subspaces.json', 'common/config/rush/version-policies.json', 'common/git-hooks/commit-msg.sample', diff --git a/libraries/rush-lib/src/schemas/subspaces.schema.json b/libraries/rush-lib/src/schemas/subspaces.schema.json index e6d62d9b2fe..5322a806a7b 100644 --- a/libraries/rush-lib/src/schemas/subspaces.schema.json +++ b/libraries/rush-lib/src/schemas/subspaces.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "title": "Rush subspace config file.", - "description": "The configuration file for enabling the subspaces feature in rush. This is an EXPERIMENTAL feature which is not yet fully implemented. To opt into the experiemnt, simply toggle the 'enabled' property in this file.", + "description": "The configuration file for enabling the subspaces feature in rush. This is an EXPERIMENTAL feature which is not yet fully implemented. To opt into the experiment, simply toggle the 'enabled' property in this file.", "type": "object", "properties": { @@ -9,7 +9,7 @@ "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", "type": "string" }, - "enabled": { + "subspacesEnabled": { "description": "If true, rush will use the subspaces configuration.", "type": "boolean" }, @@ -17,6 +17,10 @@ "description": "(DEPRECATED) Allows individual subspaces to be configured at the package level if that package is the only project in the subspace. Used to help migrate from a split workspace state.", "type": "boolean" }, + "preventSelectingAllSubspaces": { + "description": "If true, requires a selector for a subspace or set of subspaces when installing.", + "type": "boolean" + }, "subspaceNames": { "description": "Individual subspace configurations.", "type": "array",