diff --git a/apps/rush-lib/assets/rush-init/common/config/rush/build-cache.json b/apps/rush-lib/assets/rush-init/common/config/rush/build-cache.json index 04cdd9cc7a9..2a967fae966 100644 --- a/apps/rush-lib/assets/rush-init/common/config/rush/build-cache.json +++ b/apps/rush-lib/assets/rush-init/common/config/rush/build-cache.json @@ -2,7 +2,7 @@ * This configuration file manages Rush's build cache feature. * More documentation is available on the Rush website: https://rushjs.io */ - { +{ "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json", /** @@ -23,7 +23,7 @@ * Setting this property overrides the cache entry ID. If this property is set, it must contain * a [hash] token. It may also contain a [projectName] or a [projectName:normalize] token. */ - // "cacheEntryNamePattern": "[projectName:normalize]-[hash]" + // "cacheEntryNamePattern": "[projectName:normalize]-[phaseName:normalize]-[hash]" /** * Use this configuration with "cacheProvider"="azure-blob-storage" diff --git a/apps/rush-lib/src/api/CommandLineConfiguration.ts b/apps/rush-lib/src/api/CommandLineConfiguration.ts index 0f2e8c5fc6a..c258df0dd4e 100644 --- a/apps/rush-lib/src/api/CommandLineConfiguration.ts +++ b/apps/rush-lib/src/api/CommandLineConfiguration.ts @@ -2,25 +2,100 @@ // See LICENSE in the project root for license information. import * as path from 'path'; - import { JsonFile, JsonSchema, FileSystem } from '@rushstack/node-core-library'; import { RushConstants } from '../logic/RushConstants'; - -import { +import type { CommandJson, ICommandLineJson, IPhaseJson, - ParameterJson, - IPhasedCommandJson + IPhasedCommandJson, + IBulkCommandJson, + IGlobalCommandJson, + IFlagParameterJson, + IChoiceParameterJson, + IStringParameterJson } from './CommandLineJson'; -const EXPECTED_PHASE_NAME_PREFIX: '_phase:' = '_phase:'; - export interface IShellCommandTokenContext { packageFolder: string; } +export interface IPhase extends IPhaseJson { + /** + * If set to "true," this this phase was generated from a bulk command, and + * was not explicitly defined in the command-line.json file. + */ + isSynthetic: boolean; + + /** + * This property is used in the name of the filename for the logs generated by this + * phase. This is a filesystem-safe version of the phase name. For example, + * a phase with name "_phase:compile" has a `logFilenameIdentifier` of "_phase_compile". + */ + logFilenameIdentifier: string; +} + +export interface ICommandWithParameters { + associatedParameters: Set; +} +export interface IPhasedCommand extends IPhasedCommandJson, ICommandWithParameters { + /** + * If set to "true," this this phased command was generated from a bulk command, and + * was not explicitly defined in the command-line.json file. + */ + isSynthetic: boolean; + watchForChanges?: boolean; + disableBuildCache?: boolean; +} + +export interface IGlobalCommand extends IGlobalCommandJson, ICommandWithParameters {} + +export type Command = IGlobalCommand | IPhasedCommand; + +export type Parameter = IFlagParameterJson | IChoiceParameterJson | IStringParameterJson; + +const DEFAULT_BUILD_COMMAND_JSON: IBulkCommandJson = { + commandKind: RushConstants.bulkCommandKind, + name: RushConstants.buildCommandName, + summary: "Build all projects that haven't been built, or have changed since they were last built.", + description: + 'This command is similar to "rush rebuild", except that "rush build" performs' + + ' an incremental build. In other words, it only builds projects whose source files have changed' + + ' since the last successful build. The analysis requires a Git working tree, and only considers' + + ' source files that are tracked by Git and whose path is under the project folder. (For more details' + + ' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental' + + ' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The' + + ' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained' + + ' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).', + enableParallelism: true, + ignoreMissingScript: false, + ignoreDependencyOrder: false, + incremental: true, + allowWarningsInSuccessfulBuild: false, + safeForSimultaneousRushProcesses: false +}; + +const DEFAULT_REBUILD_COMMAND_JSON: IBulkCommandJson = { + commandKind: RushConstants.bulkCommandKind, + name: RushConstants.rebuildCommandName, + summary: 'Clean and rebuild the entire set of projects.', + description: + 'This command assumes that the package.json file for each project contains' + + ' a "scripts" entry for "npm run build" that performs a full clean build.' + + ' Rush invokes this script to build each project that is registered in rush.json.' + + ' Projects are built in parallel where possible, but always respecting the dependency' + + ' graph for locally linked projects. The number of simultaneous processes will be' + + ' based on the number of machine cores unless overridden by the --parallelism flag.' + + ' (For an incremental build, see "rush build" instead of "rush rebuild".)', + enableParallelism: true, + ignoreMissingScript: false, + ignoreDependencyOrder: false, + incremental: false, + allowWarningsInSuccessfulBuild: false, + safeForSimultaneousRushProcesses: false +}; + /** * Custom Commands and Options for the Rush Command Line */ @@ -29,64 +104,33 @@ export class CommandLineConfiguration { path.join(__dirname, '../schemas/command-line.schema.json') ); - public readonly commands: Map = new Map(); - public readonly phases: Map = new Map(); - public readonly parameters: ParameterJson[] = []; - private readonly _commandNames: Set = new Set([ - RushConstants.buildCommandName, - RushConstants.rebuildCommandName - ]); + public readonly commands: Map = new Map(); + public readonly phases: Map = new Map(); + public readonly parameters: Parameter[] = []; + + /** + * shellCommand from plugin custom command line configuration needs to be expanded with tokens + */ + public shellCommandTokenContext: IShellCommandTokenContext | undefined; /** * These path will be prepended to the PATH environment variable */ private _additionalPathFolders: string[] = []; + private readonly _commandsByPhaseName: Map> = new Map(); + /** - * shellCommand from plugin custom command line configuration needs to be expanded with tokens + * This maps phase names to the names of all other phases that depend on it or are + * dependent on it. This is used to determine which commands a phase affects, even + * if that phase isn't explicitly listed for that command. */ - private _shellCommandTokenContext: IShellCommandTokenContext | undefined; - - public static readonly defaultBuildCommandJson: CommandJson = { - commandKind: RushConstants.bulkCommandKind, - name: RushConstants.buildCommandName, - summary: "Build all projects that haven't been built, or have changed since they were last built.", - description: - 'This command is similar to "rush rebuild", except that "rush build" performs' + - ' an incremental build. In other words, it only builds projects whose source files have changed' + - ' since the last successful build. The analysis requires a Git working tree, and only considers' + - ' source files that are tracked by Git and whose path is under the project folder. (For more details' + - ' about this algorithm, see the documentation for the "package-deps-hash" NPM package.) The incremental' + - ' build state is tracked in a per-project folder called ".rush/temp" which should NOT be added to Git. The' + - ' build command is tracked by the "arguments" field in the "package-deps_build.json" file contained' + - ' therein; a full rebuild is forced whenever the command has changed (e.g. "--production" or not).', - enableParallelism: true, - ignoreMissingScript: false, - ignoreDependencyOrder: false, - incremental: true, - allowWarningsInSuccessfulBuild: false, - safeForSimultaneousRushProcesses: false - }; - - public static readonly defaultRebuildCommandJson: CommandJson = { - commandKind: RushConstants.bulkCommandKind, - name: RushConstants.rebuildCommandName, - summary: 'Clean and rebuild the entire set of projects', - description: - 'This command assumes that the package.json file for each project contains' + - ' a "scripts" entry for "npm run build" that performs a full clean build.' + - ' Rush invokes this script to build each project that is registered in rush.json.' + - ' Projects are built in parallel where possible, but always respecting the dependency' + - ' graph for locally linked projects. The number of simultaneous processes will be' + - ' based on the number of machine cores unless overridden by the --parallelism flag.' + - ' (For an incremental build, see "rush build" instead of "rush rebuild".)', - enableParallelism: true, - ignoreMissingScript: false, - ignoreDependencyOrder: false, - incremental: false, - allowWarningsInSuccessfulBuild: false, - safeForSimultaneousRushProcesses: false - }; + private readonly _relatedPhaseSetsByPhaseName: Map> = new Map(); + + /** + * A map of bulk command names to their corresponding synthetic phase identifiers + */ + private readonly _syntheticPhasesNamesByTranslatedBulkCommandName: Map = new Map(); /** * Use CommandLineConfiguration.loadFromFile() @@ -94,209 +138,324 @@ export class CommandLineConfiguration { * @internal */ public constructor(commandLineJson: ICommandLineJson | undefined) { - if (commandLineJson) { - if (commandLineJson.phases) { - for (const phase of commandLineJson.phases) { - if (this.phases.has(phase.name)) { - throw new Error( - `In ${RushConstants.commandLineFilename}, the phase "${phase.name}" is specified ` + - 'more than once.' - ); - } + if (commandLineJson?.phases) { + const phaseNameRegexp: RegExp = new RegExp( + `^${RushConstants.phaseNamePrefix}[a-z][a-z0-9]*([-][a-z0-9]+)*$` + ); + for (const phase of commandLineJson.phases) { + if (this.phases.has(phase.name)) { + throw new Error( + `In ${RushConstants.commandLineFilename}, the phase "${phase.name}" is specified ` + + 'more than once.' + ); + } + + if (!phase.name.match(phaseNameRegexp)) { + throw new Error( + `In ${RushConstants.commandLineFilename}, the phase "${phase.name}"'s name ` + + 'is not a valid phase name. Phase names must begin with the ' + + `required prefix "${RushConstants.phaseNamePrefix}" followed by a name containing ` + + 'lowercase letters, numbers, or hyphens. The name must start with a letter and ' + + 'must not end with a hyphen.' + ); + } - if (phase.name.substring(0, EXPECTED_PHASE_NAME_PREFIX.length) !== EXPECTED_PHASE_NAME_PREFIX) { + this.phases.set(phase.name, { + ...phase, + isSynthetic: false, + logFilenameIdentifier: this._normalizeNameForLogFilenameIdentifiers(phase.name) + }); + this._commandsByPhaseName.set(phase.name, new Set()); + } + } + + for (const phase of this.phases.values()) { + if (phase.dependencies?.self) { + for (const dependencyName of phase.dependencies.self) { + const dependency: IPhase | undefined = this.phases.get(dependencyName); + if (!dependency) { throw new Error( - `In ${RushConstants.commandLineFilename}, the phase "${phase.name}"'s name ` + - `does not begin with the required prefix "${EXPECTED_PHASE_NAME_PREFIX}".` + `In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", the self ` + + `dependency phase "${dependencyName}" does not exist.` ); } + } + } - if (phase.name.length <= EXPECTED_PHASE_NAME_PREFIX.length) { + if (phase.dependencies?.upstream) { + for (const dependency of phase.dependencies.upstream) { + if (!this.phases.has(dependency)) { throw new Error( - `In ${RushConstants.commandLineFilename}, the phase "${phase.name}"'s name ` + - `must have characters after "${EXPECTED_PHASE_NAME_PREFIX}"` + `In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", ` + + `the upstream dependency phase "${dependency}" does not exist.` ); } - - this.phases.set(phase.name, phase); } } - for (const phase of this.phases.values()) { - if (phase.dependencies?.self) { - for (const dependencyName of phase.dependencies.self) { - const dependency: IPhaseJson | undefined = this.phases.get(dependencyName); - if (!dependency) { - throw new Error( - `In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", the self ` + - `dependency phase "${dependencyName}" does not exist.` - ); - } - } - } + this._checkForPhaseSelfCycles(phase); + const relatedPhaseSet: Set = new Set(); + this._populateRelatedPhaseSets(phase.name, relatedPhaseSet); + this._relatedPhaseSetsByPhaseName.set(phase.name, relatedPhaseSet); + } - if (phase.dependencies?.upstream) { - for (const dependency of phase.dependencies.upstream) { - if (!this.phases.has(dependency)) { - throw new Error( - `In ${RushConstants.commandLineFilename}, in the phase "${phase.name}", the upstream ` + - `dependency phase "${dependency}" does not exist.` - ); - } - } + let buildCommandPhases: string[] | undefined; + if (commandLineJson?.commands) { + for (const command of commandLineJson.commands) { + if (this.commands.has(command.name)) { + throw new Error( + `In ${RushConstants.commandLineFilename}, the command "${command.name}" is specified ` + + 'more than once.' + ); } - this._checkForPhaseSelfCycles(phase); - } - - if (commandLineJson.commands) { - for (const command of commandLineJson.commands) { - if (this.commands.has(command.name)) { - throw new Error( - `In ${RushConstants.commandLineFilename}, the command "${command.name}" is specified ` + - 'more than once.' - ); - } - - if (command.commandKind === 'phased') { - const phasedCommand: IPhasedCommandJson = command as IPhasedCommandJson; - for (const phase of phasedCommand.phases) { - if (!this.phases.has(phase)) { + let normalizedCommand: Command; + switch (command.commandKind) { + case RushConstants.phasedCommandKind: { + normalizedCommand = { + ...command, + isSynthetic: false, + associatedParameters: new Set() + }; + + for (const phaseName of normalizedCommand.phases) { + if (!this.phases.has(phaseName)) { throw new Error( `In ${RushConstants.commandLineFilename}, in the "phases" property of the ` + - `"${command.name}" command, the phase "${phase}" does not exist.` + `"${normalizedCommand.name}" command, the phase "${phaseName}" does not exist.` ); } + + this._populateCommandsForPhase(phaseName, normalizedCommand); } - if (phasedCommand.skipPhasesForCommand) { - for (const phase of phasedCommand.skipPhasesForCommand) { - if (!this.phases.has(phase)) { + if (normalizedCommand.skipPhasesForCommand) { + for (const phaseName of normalizedCommand.skipPhasesForCommand) { + if (!this.phases.has(phaseName)) { throw new Error( `In ${RushConstants.commandLineFilename}, in the "skipPhasesForCommand" property of the ` + - `"${command.name}" command, the phase "${phase}" does not exist.` + `"${normalizedCommand.name}" command, the phase ` + + `"${phaseName}" does not exist.` ); } + + this._populateCommandsForPhase(phaseName, normalizedCommand); } } + + break; } - this.commands.set(command.name, command); - this._commandNames.add(command.name); + case RushConstants.globalCommandKind: { + normalizedCommand = { + ...command, + associatedParameters: new Set() + }; + break; + } + + case RushConstants.bulkCommandKind: { + // Translate the bulk command into a phased command + normalizedCommand = this._translateBulkCommandToPhasedCommand(command); + break; + } } + + if ( + normalizedCommand.name === RushConstants.buildCommandName || + normalizedCommand.name === RushConstants.rebuildCommandName + ) { + if (normalizedCommand.commandKind === RushConstants.globalCommandKind) { + throw new Error( + `${RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` + + `the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` + + `kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".` + ); + } else if (command.safeForSimultaneousRushProcesses) { + throw new Error( + `${RushConstants.commandLineFilename} defines a command "${normalizedCommand.name}" using ` + + `"safeForSimultaneousRushProcesses=true". This configuration is not supported for "${normalizedCommand.name}".` + ); + } else if (normalizedCommand.name === RushConstants.buildCommandName) { + // Record the build command phases in case we need to construct a synthetic "rebuild" command + buildCommandPhases = normalizedCommand.phases; + } + } + + this.commands.set(normalizedCommand.name, normalizedCommand); } + } - if (commandLineJson.parameters) { - for (const parameter of commandLineJson.parameters) { - this.parameters.push(parameter); - - let parameterHasAssociations: boolean = false; - - // Do some basic validation - switch (parameter.parameterKind) { - case 'flag': { - const addPhasesToCommandSet: Set = new Set(); - - if (parameter.addPhasesToCommand) { - for (const phase of parameter.addPhasesToCommand) { - addPhasesToCommandSet.add(phase); - if (!this.phases.has(phase)) { - throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}" ` + - `that lists phase "${phase}" in its "addPhasesToCommand" property that does not exist.` - ); - } else { - parameterHasAssociations = true; - } - } - } + let buildCommand: Command | undefined = this.commands.get(RushConstants.buildCommandName); + if (!buildCommand) { + // If the build command was not specified in the config file, add the default build command + buildCommand = this._translateBulkCommandToPhasedCommand(DEFAULT_BUILD_COMMAND_JSON); + buildCommand.disableBuildCache = DEFAULT_BUILD_COMMAND_JSON.disableBuildCache; + buildCommandPhases = buildCommand.phases; + this.commands.set(buildCommand.name, buildCommand); + } - if (parameter.skipPhasesForCommand) { - for (const phase of parameter.skipPhasesForCommand) { - if (!this.phases.has(phase)) { - throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}" ` + - `that lists phase "${phase}" in its skipPhasesForCommand" property that does not exist.` - ); - } else if (addPhasesToCommandSet.has(phase)) { - throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}" ` + - `that lists phase "${phase}" in both its "addPhasesToCommand" and "skipPhasesForCommand" properties.` - ); - } else { - parameterHasAssociations = true; - } - } - } + if (!this.commands.has(RushConstants.rebuildCommandName)) { + // If a rebuild command was not specified in the config file, add the default rebuild command + if (!buildCommandPhases) { + throw new Error(`Phases for the "${RushConstants.buildCommandName}" were not found.`); + } - break; - } + const rebuildCommand: IPhasedCommand = { + ...DEFAULT_REBUILD_COMMAND_JSON, + commandKind: RushConstants.phasedCommandKind, + isSynthetic: true, + phases: buildCommandPhases, + disableBuildCache: DEFAULT_REBUILD_COMMAND_JSON.disableBuildCache, + associatedParameters: buildCommand.associatedParameters // rebuild should share build's parameters in this case + }; + this.commands.set(rebuildCommand.name, rebuildCommand); + } - case 'choice': { - const alternativeNames: string[] = parameter.alternatives.map((x) => x.name); + if (commandLineJson?.parameters) { + for (const parameter of commandLineJson.parameters) { + const normalizedParameter: Parameter = { + ...parameter, + associatedPhases: parameter.associatedPhases ? [...parameter.associatedPhases] : [], + associatedCommands: parameter.associatedCommands ? [...parameter.associatedCommands] : [] + }; - if (parameter.defaultValue && alternativeNames.indexOf(parameter.defaultValue) < 0) { - throw new Error( - `In ${RushConstants.commandLineFilename}, the parameter "${parameter.longName}",` + - ` specifies a default value "${parameter.defaultValue}"` + - ` which is not one of the defined alternatives: "${alternativeNames.toString()}"` - ); + this.parameters.push(normalizedParameter); + + let parameterHasAssociations: boolean = false; + + // Do some basic validation + switch (normalizedParameter.parameterKind) { + case 'flag': { + const addPhasesToCommandSet: Set = new Set(); + + if (normalizedParameter.addPhasesToCommand) { + for (const phaseName of normalizedParameter.addPhasesToCommand) { + addPhasesToCommandSet.add(phaseName); + const phase: IPhase | undefined = this.phases.get(phaseName); + if (!phase || phase.isSynthetic) { + throw new Error( + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` + + `that lists phase "${phaseName}" in its "addPhasesToCommand" ` + + 'property that does not exist.' + ); + } else { + this._populateCommandAssociatedParametersForPhase(phaseName, normalizedParameter); + parameterHasAssociations = true; + } } + } - break; + if (normalizedParameter.skipPhasesForCommand) { + for (const phaseName of normalizedParameter.skipPhasesForCommand) { + const phase: IPhase | undefined = this.phases.get(phaseName); + if (!phase || phase.isSynthetic) { + throw new Error( + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` + + `that lists phase "${phaseName}" in its skipPhasesForCommand" ` + + 'property that does not exist.' + ); + } else if (addPhasesToCommandSet.has(phaseName)) { + throw new Error( + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` + + `that lists phase "${phaseName}" in both its "addPhasesToCommand" ` + + 'and "skipPhasesForCommand" properties.' + ); + } else { + this._populateCommandAssociatedParametersForPhase(phaseName, normalizedParameter); + parameterHasAssociations = true; + } + } } + + break; } - for (const associatedCommand of parameter.associatedCommands || []) { - if (!this._commandNames.has(associatedCommand)) { + case 'choice': { + const alternativeNames: string[] = normalizedParameter.alternatives.map((x) => x.name); + + if ( + normalizedParameter.defaultValue && + alternativeNames.indexOf(normalizedParameter.defaultValue) < 0 + ) { throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}" ` + - `that is associated with a command "${associatedCommand}" that does not exist or does ` + - 'not support custom parameters.' + `In ${RushConstants.commandLineFilename}, the parameter "${normalizedParameter.longName}",` + + ` specifies a default value "${normalizedParameter.defaultValue}"` + + ` which is not one of the defined alternatives: "${alternativeNames.toString()}"` ); - } else { - parameterHasAssociations = true; } + + break; } + } - for (const associatedPhase of parameter.associatedPhases || []) { - if (!this.phases.has(associatedPhase)) { + if (normalizedParameter.associatedCommands) { + for (let i: number = 0; i < normalizedParameter.associatedCommands.length; i++) { + const associatedCommandName: string = normalizedParameter.associatedCommands[i]; + const syntheticPhaseName: string | undefined = + this._syntheticPhasesNamesByTranslatedBulkCommandName.get(associatedCommandName); + if (syntheticPhaseName) { + // If this parameter was associated with a bulk command, change the association to + // the command's synthetic phase + normalizedParameter.associatedPhases!.push(syntheticPhaseName); + normalizedParameter.associatedCommands.splice(i, 1); + i--; + this._populateCommandAssociatedParametersForPhase(syntheticPhaseName, normalizedParameter); + parameterHasAssociations = true; + } else if (!this.commands.has(associatedCommandName)) { throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}" ` + - `that is associated with a phase "${associatedPhase}" that does not exist.` + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` + + `that is associated with a command "${associatedCommandName}" that does not exist or does ` + + 'not support custom parameters.' ); } else { + const associatedCommand: Command = this.commands.get(associatedCommandName)!; + associatedCommand.associatedParameters.add(normalizedParameter); parameterHasAssociations = true; } } + } - if (!parameterHasAssociations) { + for (const associatedPhase of normalizedParameter.associatedPhases || []) { + if (!this.phases.has(associatedPhase)) { throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${parameter.longName}"` + - ` that lists no associated commands or phases.` + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}" ` + + `that is associated with a phase "${associatedPhase}" that does not exist.` ); + } else { + this._populateCommandAssociatedParametersForPhase(associatedPhase, normalizedParameter); + parameterHasAssociations = true; } } + + if (!parameterHasAssociations) { + throw new Error( + `${RushConstants.commandLineFilename} defines a parameter "${normalizedParameter.longName}"` + + ` that lists no associated commands or phases.` + ); + } } } } - private _checkForPhaseSelfCycles(phase: IPhaseJson, checkedPhases: Set = new Set()): void { - const dependencies: string[] | undefined = phase.dependencies?.self; - if (dependencies) { - for (const dependencyName of dependencies) { + private _checkForPhaseSelfCycles(phase: IPhase, checkedPhases: Set = new Set()): void { + const phaseSelfDependencies: string[] | undefined = phase.dependencies?.self; + if (phaseSelfDependencies) { + for (const dependencyName of phaseSelfDependencies) { if (checkedPhases.has(dependencyName)) { + const dependencyNameForError: string = + typeof dependencyName === 'string' ? dependencyName : ''; throw new Error( `In ${RushConstants.commandLineFilename}, there exists a cycle within the ` + - `set of ${dependencyName} dependencies: ${Array.from(checkedPhases).join(', ')}` + `set of ${dependencyNameForError} dependencies: ${Array.from(checkedPhases).join(', ')}` ); } else { checkedPhases.add(dependencyName); - const dependency: IPhaseJson | undefined = this.phases.get(dependencyName); + const dependency: IPhase | undefined = this.phases.get(dependencyName); if (!dependency) { return; // Ignore, we check for this separately } else { - if (dependencies.length > 1) { + if (phaseSelfDependencies.length > 1) { this._checkForPhaseSelfCycles( dependency, // Clone the set of checked phases if there are multiple branches we need to check @@ -311,14 +470,34 @@ export class CommandLineConfiguration { } } + private _populateRelatedPhaseSets(phaseName: string, relatedPhaseSet: Set): void { + if (!relatedPhaseSet.has(phaseName)) { + relatedPhaseSet.add(phaseName); + const phase: IPhase = this.phases.get(phaseName)!; + if (phase.dependencies) { + if (phase.dependencies.self) { + for (const dependencyName of phase.dependencies.self) { + this._populateRelatedPhaseSets(dependencyName, relatedPhaseSet); + } + } + + if (phase.dependencies.upstream) { + for (const dependencyName of phase.dependencies.upstream) { + this._populateRelatedPhaseSets(dependencyName, relatedPhaseSet); + } + } + } + } + } + /** * Loads the configuration from the specified file and applies any omitted default build * settings. If the file does not exist, then an empty default instance is returned. * If the file contains errors, then an exception is thrown. */ - public static loadFromFileOrDefault(jsonFilename: string): CommandLineConfiguration { + public static loadFromFileOrDefault(jsonFilename?: string): CommandLineConfiguration { let commandLineJson: ICommandLineJson | undefined = undefined; - if (FileSystem.exists(jsonFilename)) { + if (jsonFilename && FileSystem.exists(jsonFilename)) { commandLineJson = JsonFile.load(jsonFilename); // merge commands specified in command-line.json and default (re)build settings @@ -333,12 +512,12 @@ export class CommandLineConfiguration { case RushConstants.bulkCommandKind: { switch (command.name) { case RushConstants.buildCommandName: { - commandDefaultDefinition = CommandLineConfiguration.defaultBuildCommandJson; + commandDefaultDefinition = DEFAULT_BUILD_COMMAND_JSON; break; } case RushConstants.rebuildCommandName: { - commandDefaultDefinition = CommandLineConfiguration.defaultRebuildCommandJson; + commandDefaultDefinition = DEFAULT_REBUILD_COMMAND_JSON; break; } } @@ -368,11 +547,58 @@ export class CommandLineConfiguration { this._additionalPathFolders.unshift(pathFolder); } - public get shellCommandTokenContext(): Readonly | undefined { - return this._shellCommandTokenContext; + /** + * This function replaces colons (":") with underscores ("_"). + * + * ts-command-line restricts command names to lowercase letters, numbers, underscores, and colons. + * Replacing colons with underscores produces a filesystem-safe name. + */ + private _normalizeNameForLogFilenameIdentifiers(name: string): string { + return name.replace(/:/g, '_'); // Replace colons with underscores to be filesystem-safe + } + + private _populateCommandsForPhase(phaseName: string, command: IPhasedCommand): void { + const populateRelatedPhaseSet: Set = this._relatedPhaseSetsByPhaseName.get(phaseName)!; + for (const relatedPhaseSetIdentifier of populateRelatedPhaseSet) { + this._commandsByPhaseName.get(relatedPhaseSetIdentifier)!.add(command); + } + } + + private _populateCommandAssociatedParametersForPhase(phaseName: string, parameter: Parameter): void { + const commands: Set = this._commandsByPhaseName.get(phaseName)!; + for (const command of commands) { + command.associatedParameters.add(parameter); + } } - public set shellCommandTokenContext(tokenContext: IShellCommandTokenContext | undefined) { - this._shellCommandTokenContext = tokenContext; + private _translateBulkCommandToPhasedCommand(command: IBulkCommandJson): IPhasedCommand { + const phaseName: string = command.name; + const phaseForBulkCommand: IPhase = { + name: phaseName, + isSynthetic: true, + dependencies: { + upstream: command.ignoreDependencyOrder ? undefined : [phaseName] + }, + ignoreMissingScript: command.ignoreMissingScript, + allowWarningsOnSuccess: command.allowWarningsInSuccessfulBuild, + logFilenameIdentifier: this._normalizeNameForLogFilenameIdentifiers(command.name) + }; + this.phases.set(phaseName, phaseForBulkCommand); + this._syntheticPhasesNamesByTranslatedBulkCommandName.set(command.name, phaseName); + const relatedPhaseSet: Set = new Set(); + this._populateRelatedPhaseSets(phaseName, relatedPhaseSet); + this._relatedPhaseSetsByPhaseName.set(phaseName, relatedPhaseSet); + + const translatedCommand: IPhasedCommand = { + ...command, + commandKind: 'phased', + disableBuildCache: true, + isSynthetic: true, + associatedParameters: new Set(), + phases: [phaseName] + }; + this._commandsByPhaseName.set(phaseName, new Set()); + this._populateCommandsForPhase(phaseName, translatedCommand); + return translatedCommand; } } diff --git a/apps/rush-lib/src/api/ExperimentsConfiguration.ts b/apps/rush-lib/src/api/ExperimentsConfiguration.ts index 45488a196a3..1a85315775e 100644 --- a/apps/rush-lib/src/api/ExperimentsConfiguration.ts +++ b/apps/rush-lib/src/api/ExperimentsConfiguration.ts @@ -42,12 +42,10 @@ export interface IExperimentsJson { buildCacheWithAllowWarningsInSuccessfulBuild?: boolean; /** - * If true, the multi-phase commands feature is enabled. To use this feature, create a "phased" command + * If true, the phased commands feature is enabled. To use this feature, create a "phased" command * in common/config/rush/command-line.json. - * - * THIS FEATURE IS NOT READY FOR USAGE YET. SEE GITHUB #2300 FOR STATUS. */ - _multiPhaseCommands?: boolean; + phasedCommands?: boolean; } /** diff --git a/apps/rush-lib/src/api/RushProjectConfiguration.ts b/apps/rush-lib/src/api/RushProjectConfiguration.ts index 762a471f5a2..ea192088f1f 100644 --- a/apps/rush-lib/src/api/RushProjectConfiguration.ts +++ b/apps/rush-lib/src/api/RushProjectConfiguration.ts @@ -154,19 +154,15 @@ export class RushProjectConfiguration { public readonly project: RushConfigurationProject; - /** - * A list of folder names under the project root that should be cached. - * - * These folders should not be tracked by git. - */ - public readonly projectOutputFolderNames?: ReadonlyArray; - /** * A list of folder names under the project root that should be cached for each phase. * * These folders should not be tracked by git. + * + * @remarks + * The `projectOutputFolderNames` property is populated in a "build" entry */ - public readonly projectOutputFolderNamesForPhases?: ReadonlyMap>; + public readonly projectOutputFolderNamesForPhases: ReadonlyMap>; /** * The incremental analyzer can skip Rush commands for projects whose input files have @@ -185,12 +181,10 @@ export class RushProjectConfiguration { private constructor( project: RushConfigurationProject, rushProjectJson: IRushProjectJson, - projectOutputFolderNamesForPhases: ReadonlyMap> | undefined + projectOutputFolderNamesForPhases: ReadonlyMap> ) { this.project = project; - this.projectOutputFolderNames = rushProjectJson.projectOutputFolderNames; - this.projectOutputFolderNamesForPhases = projectOutputFolderNamesForPhases; this.incrementalBuildIgnoredGlobs = rushProjectJson.incrementalBuildIgnoredGlobs; @@ -215,28 +209,20 @@ export class RushProjectConfiguration { */ public static async tryLoadForProjectAsync( project: RushConfigurationProject, - repoCommandLineConfiguration: CommandLineConfiguration | undefined, - terminal: ITerminal, - skipCache?: boolean + repoCommandLineConfiguration: CommandLineConfiguration, + terminal: ITerminal ): Promise { // false is a signal that the project config does not exist - const cacheEntry: RushProjectConfiguration | false | undefined = skipCache - ? undefined - : RushProjectConfiguration._configCache.get(project); + const cacheEntry: RushProjectConfiguration | false | undefined = + RushProjectConfiguration._configCache.get(project); if (cacheEntry !== undefined) { return cacheEntry || undefined; } - const rigConfig: RigConfig = await RigConfig.loadForProjectFolderAsync({ - projectFolderPath: project.projectFolder - }); - - const rushProjectJson: IRushProjectJson | undefined = - await this._projectBuildCacheConfigurationFile.tryLoadConfigurationFileForProjectAsync( - terminal, - project.projectFolder, - rigConfig - ); + const rushProjectJson: IRushProjectJson | undefined = await this._tryLoadJsonForProjectAsync( + project, + terminal + ); if (rushProjectJson) { const result: RushProjectConfiguration = RushProjectConfiguration._getRushProjectConfiguration( @@ -253,10 +239,48 @@ export class RushProjectConfiguration { } } + /** + * Load only the `incrementalBuildIgnoredGlobs` property from the rush-project.json file, skipping + * validation of other parts of the config file. + * + * @remarks + * This function exists to allow the ProjectChangeAnalyzer to load just the ignore globs without + * having to validate the rest of the `rush-project.json` file against the repo's command-line configuration. + */ + public static async tryLoadIgnoreGlobsForProjectAsync( + project: RushConfigurationProject, + terminal: ITerminal + ): Promise | undefined> { + const rushProjectJson: IRushProjectJson | undefined = await this._tryLoadJsonForProjectAsync( + project, + terminal + ); + + return rushProjectJson?.incrementalBuildIgnoredGlobs; + } + + private static async _tryLoadJsonForProjectAsync( + project: RushConfigurationProject, + terminal: ITerminal + ): Promise { + const rigConfig: RigConfig = await RigConfig.loadForProjectFolderAsync({ + projectFolderPath: project.projectFolder + }); + + const rushProjectJson: IRushProjectJson | undefined = + await this._projectBuildCacheConfigurationFile.tryLoadConfigurationFileForProjectAsync( + terminal, + project.projectFolder, + rigConfig + ); + + return rushProjectJson; + } + private static _getRushProjectConfiguration( project: RushConfigurationProject, rushProjectJson: IRushProjectJson, - repoCommandLineConfiguration: CommandLineConfiguration | undefined, + repoCommandLineConfiguration: CommandLineConfiguration, terminal: ITerminal ): RushProjectConfiguration { if (rushProjectJson.projectOutputFolderNames) { @@ -293,15 +317,10 @@ export class RushProjectConfiguration { const duplicateCommandNames: Set = new Set(); const invalidCommandNames: string[] = []; if (rushProjectJson.buildCacheOptions?.optionsForCommands) { - const commandNames: Set = new Set([ - RushConstants.buildCommandName, - RushConstants.rebuildCommandName - ]); - if (repoCommandLineConfiguration) { - for (const [commandName, command] of repoCommandLineConfiguration.commands) { - if (command.commandKind === RushConstants.bulkCommandKind) { - commandNames.add(commandName); - } + const commandNames: Set = new Set(); + for (const [commandName, command] of repoCommandLineConfiguration.commands) { + if (command.commandKind === RushConstants.phasedCommandKind) { + commandNames.add(commandName); } } @@ -334,11 +353,16 @@ export class RushProjectConfiguration { ); } - let projectOutputFolderNamesForPhases: Map | undefined; + const projectOutputFolderNamesForPhases: Map = new Map(); + if (rushProjectJson.projectOutputFolderNames) { + projectOutputFolderNamesForPhases.set( + RushConstants.buildCommandName, + rushProjectJson.projectOutputFolderNames + ); + } + if (rushProjectJson.phaseOptions) { const overlappingPathAnalyzer: OverlappingPathAnalyzer = new OverlappingPathAnalyzer(); - - projectOutputFolderNamesForPhases = new Map(); const phaseOptionsByPhase: Map = new Map< string, IRushProjectJsonPhaseOptionsJson @@ -375,7 +399,7 @@ export class RushProjectConfiguration { } terminal.writeErrorLine(errorMessage); - } else if (!repoCommandLineConfiguration?.phases.has(phaseName)) { + } else if (!repoCommandLineConfiguration.phases.has(phaseName)) { terminal.writeErrorLine( `Invalid "${RushProjectConfiguration._projectBuildCacheConfigurationFile.projectRelativeFilePath}"` + ` for project "${project.packageName}". Phase "${phaseName}" is not defined in the repo's ${RushConstants.commandLineFilename}.` @@ -401,7 +425,7 @@ export class RushProjectConfiguration { terminal.writeErrorLine( `Invalid "${RushProjectConfiguration._projectBuildCacheConfigurationFile.projectRelativeFilePath}" ` + `for project "${project.packageName}". The project output folder name "${projectOutputFolderName}" in ` + - `phase ${phaseName} overlaps with other another folder name in the same phase.` + `phase ${phaseName} overlaps with another folder name in the same phase.` ); } else { const otherPhaseNames: string[] = overlappingPhaseNames.filter( @@ -410,7 +434,7 @@ export class RushProjectConfiguration { terminal.writeErrorLine( `Invalid "${RushProjectConfiguration._projectBuildCacheConfigurationFile.projectRelativeFilePath}" ` + `for project "${project.packageName}". The project output folder name "${projectOutputFolderName}" in ` + - `phase ${phaseName} overlaps with other another folder names in the same phase and with ` + + `phase ${phaseName} overlaps with other folder names in the same phase and with ` + `folder names in the following other phases: ${otherPhaseNames.join(', ')}.` ); } diff --git a/apps/rush-lib/src/api/test/CommandLineConfiguration.test.ts b/apps/rush-lib/src/api/test/CommandLineConfiguration.test.ts index 2f60c11dcd5..d3c0082b927 100644 --- a/apps/rush-lib/src/api/test/CommandLineConfiguration.test.ts +++ b/apps/rush-lib/src/api/test/CommandLineConfiguration.test.ts @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { CommandLineConfiguration } from '../CommandLineConfiguration'; +import { RushConstants } from '../../logic/RushConstants'; +import { Command, CommandLineConfiguration, Parameter } from '../CommandLineConfiguration'; -describe('CommandLineConfiguration', () => { +describe(CommandLineConfiguration.name, () => { it('Forbids a misnamed phase', () => { expect( () => @@ -25,6 +26,36 @@ describe('CommandLineConfiguration', () => { ] }) ).toThrowErrorMatchingSnapshot(); + expect( + () => + new CommandLineConfiguration({ + phases: [ + { + name: '_phase:0' + } + ] + }) + ).toThrowErrorMatchingSnapshot(); + expect( + () => + new CommandLineConfiguration({ + phases: [ + { + name: '_phase:A' + } + ] + }) + ).toThrowErrorMatchingSnapshot(); + expect( + () => + new CommandLineConfiguration({ + phases: [ + { + name: '_phase:A-' + } + ] + }) + ).toThrowErrorMatchingSnapshot(); }); it('Detects a missing phase', () => { @@ -40,7 +71,7 @@ describe('CommandLineConfiguration', () => { safeForSimultaneousRushProcesses: false, enableParallelism: true, - phases: ['_phase:A'] + phases: ['_phase:a'] } ] }) @@ -53,9 +84,9 @@ describe('CommandLineConfiguration', () => { new CommandLineConfiguration({ phases: [ { - name: '_phase:A', + name: '_phase:a', dependencies: { - upstream: ['_phase:B'] + upstream: ['_phase:b'] } } ] @@ -67,9 +98,9 @@ describe('CommandLineConfiguration', () => { new CommandLineConfiguration({ phases: [ { - name: '_phase:A', + name: '_phase:a', dependencies: { - self: ['_phase:B'] + self: ['_phase:b'] } } ] @@ -83,25 +114,153 @@ describe('CommandLineConfiguration', () => { new CommandLineConfiguration({ phases: [ { - name: '_phase:A', + name: '_phase:a', dependencies: { - self: ['_phase:B'] + self: ['_phase:b'] } }, { - name: '_phase:B', + name: '_phase:b', dependencies: { - self: ['_phase:C'] + self: ['_phase:c'] } }, { - name: '_phase:C', + name: '_phase:c', dependencies: { - self: ['_phase:A'] + self: ['_phase:a'] } } ] }) ).toThrowErrorMatchingSnapshot(); }); + + describe('associatedParameters', () => { + it('correctly populates the associatedParameters object for a parameter associated with the "build" command', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + parameters: [ + { + parameterKind: 'flag', + longName: '--flag', + associatedCommands: ['build'], + description: 'flag' + } + ] + }); + + function validateCommandByName(commandName: string): void { + const command: Command | undefined = commandLineConfiguration.commands.get(commandName); + expect(command).toBeDefined(); + const associatedParametersArray: Parameter[] = Array.from(command!.associatedParameters); + expect(associatedParametersArray).toHaveLength(1); + expect(associatedParametersArray[0].longName).toEqual('--flag'); + } + + validateCommandByName(RushConstants.buildCommandName); + validateCommandByName(RushConstants.rebuildCommandName); + }); + + it('correctly populates the associatedParameters object for a parameter associated with a custom bulk command', () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'bulk', + name: 'custom-bulk', + summary: 'custom-bulk', + enableParallelism: true, + safeForSimultaneousRushProcesses: false + } + ], + parameters: [ + { + parameterKind: 'flag', + longName: '--flag', + associatedCommands: ['custom-bulk'], + description: 'flag' + } + ] + }); + + const command: Command | undefined = commandLineConfiguration.commands.get('custom-bulk'); + expect(command).toBeDefined(); + const associatedParametersArray: Parameter[] = Array.from(command!.associatedParameters); + expect(associatedParametersArray).toHaveLength(1); + expect(associatedParametersArray[0].longName).toEqual('--flag'); + }); + + it("correctly populates the associatedParameters object for a parameter associated with a custom phased command's phase", () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'phased', + name: 'custom-phased', + summary: 'custom-phased', + enableParallelism: true, + safeForSimultaneousRushProcesses: false, + phases: ['_phase:a'] + } + ], + phases: [ + { + name: '_phase:a' + } + ], + parameters: [ + { + parameterKind: 'flag', + longName: '--flag', + associatedPhases: ['_phase:a'], + description: 'flag' + } + ] + }); + + const command: Command | undefined = commandLineConfiguration.commands.get('custom-phased'); + expect(command).toBeDefined(); + const associatedParametersArray: Parameter[] = Array.from(command!.associatedParameters); + expect(associatedParametersArray).toHaveLength(1); + expect(associatedParametersArray[0].longName).toEqual('--flag'); + }); + + it("correctly populates the associatedParameters object for a parameter associated with a custom phased command's transitive phase", () => { + const commandLineConfiguration: CommandLineConfiguration = new CommandLineConfiguration({ + commands: [ + { + commandKind: 'phased', + name: 'custom-phased', + summary: 'custom-phased', + enableParallelism: true, + safeForSimultaneousRushProcesses: false, + phases: ['_phase:a'] + } + ], + phases: [ + { + name: '_phase:a', + dependencies: { + upstream: ['_phase:b'] + } + }, + { + name: '_phase:b' + } + ], + parameters: [ + { + parameterKind: 'flag', + longName: '--flag', + associatedPhases: ['_phase:b'], + description: 'flag' + } + ] + }); + + const command: Command | undefined = commandLineConfiguration.commands.get('custom-phased'); + expect(command).toBeDefined(); + const associatedParametersArray: Parameter[] = Array.from(command!.associatedParameters); + expect(associatedParametersArray).toHaveLength(1); + expect(associatedParametersArray[0].longName).toEqual('--flag'); + }); + }); }); diff --git a/apps/rush-lib/src/api/test/__snapshots__/CommandLineConfiguration.test.ts.snap b/apps/rush-lib/src/api/test/__snapshots__/CommandLineConfiguration.test.ts.snap index 83e3d560365..55015a7af18 100644 --- a/apps/rush-lib/src/api/test/__snapshots__/CommandLineConfiguration.test.ts.snap +++ b/apps/rush-lib/src/api/test/__snapshots__/CommandLineConfiguration.test.ts.snap @@ -1,13 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`CommandLineConfiguration Detects a cycle among phases 1`] = `"In command-line.json, there exists a cycle within the set of _phase:B dependencies: _phase:B, _phase:C, _phase:A"`; +exports[`CommandLineConfiguration Detects a cycle among phases 1`] = `"In command-line.json, there exists a cycle within the set of _phase:b dependencies: _phase:b, _phase:c, _phase:a"`; -exports[`CommandLineConfiguration Detects a missing phase 1`] = `"In command-line.json, in the \\"phases\\" property of the \\"example\\" command, the phase \\"_phase:A\\" does not exist."`; +exports[`CommandLineConfiguration Detects a missing phase 1`] = `"In command-line.json, in the \\"phases\\" property of the \\"example\\" command, the phase \\"_phase:a\\" does not exist."`; -exports[`CommandLineConfiguration Detects a missing phase dependency 1`] = `"In command-line.json, in the phase \\"_phase:A\\", the upstream dependency phase \\"_phase:B\\" does not exist."`; +exports[`CommandLineConfiguration Detects a missing phase dependency 1`] = `"In command-line.json, in the phase \\"_phase:a\\", the upstream dependency phase \\"_phase:b\\" does not exist."`; -exports[`CommandLineConfiguration Detects a missing phase dependency 2`] = `"In command-line.json, in the phase \\"_phase:A\\", the self dependency phase \\"_phase:B\\" does not exist."`; +exports[`CommandLineConfiguration Detects a missing phase dependency 2`] = `"In command-line.json, in the phase \\"_phase:a\\", the self dependency phase \\"_phase:b\\" does not exist."`; -exports[`CommandLineConfiguration Forbids a misnamed phase 1`] = `"In command-line.json, the phase \\"_faze:A\\"'s name does not begin with the required prefix \\"_phase:\\"."`; +exports[`CommandLineConfiguration Forbids a misnamed phase 1`] = `"In command-line.json, the phase \\"_faze:A\\"'s name is not a valid phase name. Phase names must begin with the required prefix \\"_phase:\\" followed by a name containing lowercase letters, numbers, or hyphens. The name must start with a letter and must not end with a hyphen."`; -exports[`CommandLineConfiguration Forbids a misnamed phase 2`] = `"In command-line.json, the phase \\"_phase:\\"'s name must have characters after \\"_phase:\\""`; +exports[`CommandLineConfiguration Forbids a misnamed phase 2`] = `"In command-line.json, the phase \\"_phase:\\"'s name is not a valid phase name. Phase names must begin with the required prefix \\"_phase:\\" followed by a name containing lowercase letters, numbers, or hyphens. The name must start with a letter and must not end with a hyphen."`; + +exports[`CommandLineConfiguration Forbids a misnamed phase 3`] = `"In command-line.json, the phase \\"_phase:0\\"'s name is not a valid phase name. Phase names must begin with the required prefix \\"_phase:\\" followed by a name containing lowercase letters, numbers, or hyphens. The name must start with a letter and must not end with a hyphen."`; + +exports[`CommandLineConfiguration Forbids a misnamed phase 4`] = `"In command-line.json, the phase \\"_phase:A\\"'s name is not a valid phase name. Phase names must begin with the required prefix \\"_phase:\\" followed by a name containing lowercase letters, numbers, or hyphens. The name must start with a letter and must not end with a hyphen."`; + +exports[`CommandLineConfiguration Forbids a misnamed phase 5`] = `"In command-line.json, the phase \\"_phase:A-\\"'s name is not a valid phase name. Phase names must begin with the required prefix \\"_phase:\\" followed by a name containing lowercase letters, numbers, or hyphens. The name must start with a letter and must not end with a hyphen."`; diff --git a/apps/rush-lib/src/cli/RushCommandLineParser.ts b/apps/rush-lib/src/cli/RushCommandLineParser.ts index ead36b52fd3..396ab5dd80a 100644 --- a/apps/rush-lib/src/cli/RushCommandLineParser.ts +++ b/apps/rush-lib/src/cli/RushCommandLineParser.ts @@ -16,8 +16,12 @@ import { PrintUtilities } from '@rushstack/terminal'; import { RushConfiguration } from '../api/RushConfiguration'; import { RushConstants } from '../logic/RushConstants'; -import { CommandLineConfiguration } from '../api/CommandLineConfiguration'; -import { CommandJson } from '../api/CommandLineJson'; +import { + Command, + CommandLineConfiguration, + IGlobalCommand, + IPhasedCommand +} from '../api/CommandLineConfiguration'; import { Utilities } from '../utilities/Utilities'; import { AddAction } from './actions/AddAction'; @@ -38,19 +42,17 @@ import { UpdateAction } from './actions/UpdateAction'; import { UpdateAutoinstallerAction } from './actions/UpdateAutoinstallerAction'; import { VersionAction } from './actions/VersionAction'; import { UpdateCloudCredentialsAction } from './actions/UpdateCloudCredentialsAction'; -import { WriteBuildCacheAction } from './actions/WriteBuildCacheAction'; -import { BulkScriptAction } from './scriptActions/BulkScriptAction'; import { GlobalScriptAction } from './scriptActions/GlobalScriptAction'; +import { IBaseScriptActionOptions } from './scriptActions/BaseScriptAction'; import { Telemetry } from '../logic/Telemetry'; import { RushGlobalFolder } from '../api/RushGlobalFolder'; import { NodeJsCompatibility } from '../logic/NodeJsCompatibility'; import { SetupAction } from './actions/SetupAction'; -import { EnvironmentConfiguration } from '../api/EnvironmentConfiguration'; import { ICustomCommandLineConfigurationInfo, PluginManager } from '../pluginFramework/PluginManager'; import { RushSession } from '../pluginFramework/RushSession'; -import { ProjectLogWritable } from '../logic/taskExecution/ProjectLogWritable'; +import { PhasedScriptAction } from './scriptActions/PhasedScriptAction'; /** * Options for `RushCommandLineParser`. @@ -225,7 +227,6 @@ export class RushCommandLineParser extends CommandLineParser { this.addAction(new UpdateAutoinstallerAction(this)); this.addAction(new UpdateCloudCredentialsAction(this)); this.addAction(new VersionAction(this)); - this.addAction(new WriteBuildCacheAction(this)); this._populateScriptActions(); } catch (error) { @@ -234,48 +235,22 @@ export class RushCommandLineParser extends CommandLineParser { } private _populateScriptActions(): void { - let commandLineConfiguration: CommandLineConfiguration | undefined = undefined; - // If there is not a rush.json file, we still want "build" and "rebuild" to appear in the // command-line help + let commandLineConfigFilePath: string | undefined; if (this.rushConfiguration) { - const commandLineConfigFilePath: string = path.join( + commandLineConfigFilePath = path.join( this.rushConfiguration.commonRushConfigFolder, RushConstants.commandLineFilename ); - - commandLineConfiguration = CommandLineConfiguration.loadFromFileOrDefault(commandLineConfigFilePath); } - // Build actions from the command line configuration supersede default build actions. + const commandLineConfiguration: CommandLineConfiguration = + CommandLineConfiguration.loadFromFileOrDefault(commandLineConfigFilePath); this._addCommandLineConfigActions(commandLineConfiguration); - this._addDefaultBuildActions(commandLineConfiguration); - } - - private _addDefaultBuildActions(commandLineConfiguration?: CommandLineConfiguration): void { - if (!this.tryGetAction(RushConstants.buildCommandName)) { - this._addCommandLineConfigAction( - commandLineConfiguration, - CommandLineConfiguration.defaultBuildCommandJson - ); - } - - if (!this.tryGetAction(RushConstants.rebuildCommandName)) { - // By default, the "rebuild" action runs the "build" script. However, if the command-line.json file - // overrides "rebuild," the "rebuild" script should be run. - this._addCommandLineConfigAction( - commandLineConfiguration, - CommandLineConfiguration.defaultRebuildCommandJson, - RushConstants.buildCommandName - ); - } } - private _addCommandLineConfigActions(commandLineConfiguration?: CommandLineConfiguration): void { - if (!commandLineConfiguration) { - return; - } - + private _addCommandLineConfigActions(commandLineConfiguration: CommandLineConfiguration): void { // Register each custom command for (const command of commandLineConfiguration.commands.values()) { this._addCommandLineConfigAction(commandLineConfiguration, command); @@ -283,9 +258,8 @@ export class RushCommandLineParser extends CommandLineParser { } private _addCommandLineConfigAction( - commandLineConfiguration: CommandLineConfiguration | undefined, - command: CommandJson, - commandToRun: string = command.name + commandLineConfiguration: CommandLineConfiguration, + command: Command ): void { if (this.tryGetAction(command.name)) { throw new Error( @@ -294,103 +268,105 @@ export class RushCommandLineParser extends CommandLineParser { ); } - if ( - (command.name === RushConstants.buildCommandName || - command.name === RushConstants.rebuildCommandName) && - command.safeForSimultaneousRushProcesses - ) { - // Build and rebuild can't be designated "safeForSimultaneousRushProcesses" - throw new Error( - `${RushConstants.commandLineFilename} defines a command "${command.name}" using ` + - `"safeForSimultaneousRushProcesses=true". This configuration is not supported for "${command.name}".` - ); - } - - const overrideAllowWarnings: boolean = EnvironmentConfiguration.allowWarningsInSuccessfulBuild; - switch (command.commandKind) { - case RushConstants.bulkCommandKind: { - const logFilenameIdentifier: string = - ProjectLogWritable.normalizeNameForLogFilenameIdentifiers(commandToRun); - - this.addAction( - new BulkScriptAction({ - actionName: command.name, - logFilenameIdentifier: logFilenameIdentifier, - commandToRun: commandToRun, - - summary: command.summary, - documentation: command.description || command.summary, - safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses, - - parser: this, - commandLineConfiguration: commandLineConfiguration, - - enableParallelism: command.enableParallelism, - ignoreMissingScript: command.ignoreMissingScript || false, - ignoreDependencyOrder: command.ignoreDependencyOrder || false, - incremental: command.incremental || false, - allowWarningsInSuccessfulBuild: overrideAllowWarnings || !!command.allowWarningsInSuccessfulBuild, - - watchForChanges: command.watchForChanges || false, - disableBuildCache: command.disableBuildCache || false - }) - ); - break; - } - case RushConstants.globalCommandKind: { - if ( - command.name === RushConstants.buildCommandName || - command.name === RushConstants.rebuildCommandName - ) { - throw new Error( - `${RushConstants.commandLineFilename} defines a command "${command.name}" using ` + - `the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` + - `kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".` - ); - } - - this.addAction( - new GlobalScriptAction({ - actionName: command.name, - summary: command.summary, - documentation: command.description || command.summary, - safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses, - - parser: this, - commandLineConfiguration: commandLineConfiguration, - - shellCommand: command.shellCommand, - - autoinstallerName: command.autoinstallerName - }) - ); + this._addGlobalScriptAction(commandLineConfiguration, command); break; } case RushConstants.phasedCommandKind: { - if (!this.rushConfiguration.experimentsConfiguration.configuration._multiPhaseCommands) { + if ( + !command.isSynthetic && // synthetic commands come from bulk commands + !this.rushConfiguration.experimentsConfiguration.configuration.phasedCommands + ) { throw new Error( `${RushConstants.commandLineFilename} defines a command "${command.name}" ` + `that uses the "${RushConstants.phasedCommandKind}" command kind. To use this command kind, ` + - 'the "_multiPhaseCommands" experiment must be enabled. Note that this feature is not complete ' + + 'the "phasedCommands" experiment must be enabled. Note that this feature is not complete ' + 'and will not work as expected.' ); } - // TODO + this._addPhasedCommandLineConfigAction(commandLineConfiguration, command); break; } default: throw new Error( - `${RushConstants.commandLineFilename} defines a command "${(command as CommandJson).name}"` + - ` using an unsupported command kind "${(command as CommandJson).commandKind}"` + `${RushConstants.commandLineFilename} defines a command "${(command as Command).name}"` + + ` using an unsupported command kind "${(command as Command).commandKind}"` ); } } + private _getSharedCommandActionOptions( + commandLineConfiguration: CommandLineConfiguration, + command: TCommand + ): IBaseScriptActionOptions { + return { + actionName: command.name, + summary: command.summary, + documentation: command.description || command.summary, + safeForSimultaneousRushProcesses: command.safeForSimultaneousRushProcesses, + + command, + parser: this, + commandLineConfiguration: commandLineConfiguration + }; + } + + private _addGlobalScriptAction( + commandLineConfiguration: CommandLineConfiguration, + command: IGlobalCommand + ): void { + if ( + command.name === RushConstants.buildCommandName || + command.name === RushConstants.rebuildCommandName + ) { + throw new Error( + `${RushConstants.commandLineFilename} defines a command "${command.name}" using ` + + `the command kind "${RushConstants.globalCommandKind}". This command can only be designated as a command ` + + `kind "${RushConstants.bulkCommandKind}" or "${RushConstants.phasedCommandKind}".` + ); + } + + const sharedCommandOptions: IBaseScriptActionOptions = + this._getSharedCommandActionOptions(commandLineConfiguration, command); + + this.addAction( + new GlobalScriptAction({ + ...sharedCommandOptions, + + shellCommand: command.shellCommand, + autoinstallerName: command.autoinstallerName + }) + ); + } + + private _addPhasedCommandLineConfigAction( + commandLineConfiguration: CommandLineConfiguration, + command: IPhasedCommand + ): void { + const baseCommandOptions: IBaseScriptActionOptions = this._getSharedCommandActionOptions( + commandLineConfiguration, + command + ); + + this.addAction( + new PhasedScriptAction({ + ...baseCommandOptions, + + enableParallelism: command.enableParallelism, + incremental: command.incremental || false, + watchForChanges: command.watchForChanges || false, + disableBuildCache: command.disableBuildCache || false, + + actionPhases: command.phases, + phases: commandLineConfiguration.phases + }) + ); + } + private _reportErrorAndSetExitCode(error: Error): void { if (!(error instanceof AlreadyReportedError)) { const prefix: string = 'ERROR: '; diff --git a/apps/rush-lib/src/cli/actions/WriteBuildCacheAction.ts b/apps/rush-lib/src/cli/actions/WriteBuildCacheAction.ts deleted file mode 100644 index b220e956fce..00000000000 --- a/apps/rush-lib/src/cli/actions/WriteBuildCacheAction.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import * as path from 'path'; -import { AlreadyReportedError, ConsoleTerminalProvider, Terminal } from '@rushstack/node-core-library'; -import { CommandLineFlagParameter, CommandLineStringParameter } from '@rushstack/ts-command-line'; - -import { RushConfigurationProject } from '../../api/RushConfigurationProject'; -import { BaseRushAction } from './BaseRushAction'; -import { RushCommandLineParser } from '../RushCommandLineParser'; - -import { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; -import { ProjectTaskRunner } from '../../logic/taskExecution/ProjectTaskRunner'; -import { ProjectChangeAnalyzer } from '../../logic/ProjectChangeAnalyzer'; -import { Utilities } from '../../utilities/Utilities'; -import { NonPhasedProjectTaskSelector } from '../../logic/NonPhasedProjectTaskSelector'; -import { RushConstants } from '../../logic/RushConstants'; -import { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; -import { ProjectLogWritable } from '../../logic/taskExecution/ProjectLogWritable'; - -export class WriteBuildCacheAction extends BaseRushAction { - private _command!: CommandLineStringParameter; - private _verboseFlag!: CommandLineFlagParameter; - - public constructor(parser: RushCommandLineParser) { - super({ - actionName: 'write-build-cache', - summary: 'Writes the current state of the current project to the cache.', - documentation: - '(EXPERIMENTAL) If the build cache is configured, when this command is run in the folder of ' + - 'a project, write the current state of the project to the cache.', - safeForSimultaneousRushProcesses: true, - parser - }); - } - - public onDefineParameters(): void { - this._command = this.defineStringParameter({ - parameterLongName: '--command', - parameterShortName: '-c', - required: true, - argumentName: 'COMMAND', - description: - '(Required) The command run in the current project that produced the current project state.' - }); - - this._verboseFlag = this.defineFlagParameter({ - parameterLongName: '--verbose', - parameterShortName: '-v', - description: 'Display verbose log information.' - }); - } - - public async runAsync(): Promise { - const project: RushConfigurationProject | undefined = this.rushConfiguration.tryGetProjectForPath( - process.cwd() - ); - - if (!project) { - throw new Error( - `The "rush ${this.actionName}" command must be invoked under a project` + - ` folder that is registered in rush.json.` - ); - } - - const terminal: Terminal = new Terminal( - new ConsoleTerminalProvider({ verboseEnabled: this._verboseFlag.value }) - ); - - const buildCacheConfiguration: BuildCacheConfiguration = - await BuildCacheConfiguration.loadAndRequireEnabledAsync( - terminal, - this.rushConfiguration, - this.rushSession - ); - - const command: string = this._command.value!; - const commandToRun: string | undefined = NonPhasedProjectTaskSelector.getScriptToRun( - project, - command, - [] - ); - - // TODO: With phased commands, this will need to be updated - const taskName: string = NonPhasedProjectTaskSelector.getTaskNameForProject(project); - const projectChangeAnalyzer: ProjectChangeAnalyzer = new ProjectChangeAnalyzer(this.rushConfiguration); - const projectBuilder: ProjectTaskRunner = new ProjectTaskRunner({ - rushProject: project, - taskName, - rushConfiguration: this.rushConfiguration, - buildCacheConfiguration, - commandName: command, - commandToRun: commandToRun || '', - isIncrementalBuildAllowed: false, - projectChangeAnalyzer, - packageDepsFilename: Utilities.getPackageDepsFilenameForCommand(command), - logFilenameIdentifier: ProjectLogWritable.normalizeNameForLogFilenameIdentifiers(command) - }); - - const trackedFiles: string[] = Array.from( - (await projectChangeAnalyzer._tryGetProjectDependenciesAsync(project, terminal))!.keys() - ); - const commandLineConfigFilePath: string = path.join( - this.rushConfiguration.commonRushConfigFolder, - RushConstants.commandLineFilename - ); - const repoCommandLineConfiguration: CommandLineConfiguration | undefined = - CommandLineConfiguration.loadFromFileOrDefault(commandLineConfigFilePath); - - const cacheWriteSuccess: boolean | undefined = await projectBuilder.tryWriteCacheEntryAsync( - terminal, - trackedFiles, - repoCommandLineConfiguration - ); - if (cacheWriteSuccess === undefined) { - terminal.writeErrorLine('This project does not support caching or Git is not present.'); - throw new AlreadyReportedError(); - } else if (cacheWriteSuccess === false) { - terminal.writeErrorLine('Writing cache entry failed.'); - } - } -} diff --git a/apps/rush-lib/src/cli/scriptActions/BaseScriptAction.ts b/apps/rush-lib/src/cli/scriptActions/BaseScriptAction.ts index b6f1c81c3f1..af0eb470ed4 100644 --- a/apps/rush-lib/src/cli/scriptActions/BaseScriptAction.ts +++ b/apps/rush-lib/src/cli/scriptActions/BaseScriptAction.ts @@ -3,15 +3,16 @@ import { CommandLineParameter } from '@rushstack/ts-command-line'; import { BaseRushAction, IBaseRushActionOptions } from '../actions/BaseRushAction'; -import { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; +import { Command, CommandLineConfiguration } from '../../api/CommandLineConfiguration'; import { RushConstants } from '../../logic/RushConstants'; import type { ParameterJson } from '../../api/CommandLineJson'; /** * Constructor parameters for BaseScriptAction */ -export interface IBaseScriptActionOptions extends IBaseRushActionOptions { - commandLineConfiguration: CommandLineConfiguration | undefined; +export interface IBaseScriptActionOptions extends IBaseRushActionOptions { + commandLineConfiguration: CommandLineConfiguration; + command: TCommand; } /** @@ -24,71 +25,64 @@ export interface IBaseScriptActionOptions extends IBaseRushActionOptions { * * The two subclasses are BulkScriptAction and GlobalScriptAction. */ -export abstract class BaseScriptAction extends BaseRushAction { - protected readonly _commandLineConfiguration: CommandLineConfiguration | undefined; +export abstract class BaseScriptAction extends BaseRushAction { + protected readonly commandLineConfiguration: CommandLineConfiguration; protected readonly customParameters: CommandLineParameter[] = []; + protected readonly command: TCommand; - public constructor(options: IBaseScriptActionOptions) { + public constructor(options: IBaseScriptActionOptions) { super(options); - this._commandLineConfiguration = options.commandLineConfiguration; + this.commandLineConfiguration = options.commandLineConfiguration; + this.command = options.command; } protected defineScriptParameters(): void { - if (!this._commandLineConfiguration) { + if (!this.commandLineConfiguration) { return; } // Find any parameters that are associated with this command - for (const parameterJson of this._commandLineConfiguration.parameters) { - let associated: boolean = false; - for (const associatedCommand of parameterJson.associatedCommands || []) { - if (associatedCommand === this.actionName) { - associated = true; - } - } - - if (associated) { + for (const parameter of this.commandLineConfiguration.parameters) { + if (this.command.associatedParameters.has(parameter)) { let customParameter: CommandLineParameter | undefined; - switch (parameterJson.parameterKind) { + switch (parameter.parameterKind) { case 'flag': customParameter = this.defineFlagParameter({ - parameterShortName: parameterJson.shortName, - parameterLongName: parameterJson.longName, - description: parameterJson.description, - required: parameterJson.required + parameterShortName: parameter.shortName, + parameterLongName: parameter.longName, + description: parameter.description, + required: parameter.required }); break; case 'choice': customParameter = this.defineChoiceParameter({ - parameterShortName: parameterJson.shortName, - parameterLongName: parameterJson.longName, - description: parameterJson.description, - required: parameterJson.required, - alternatives: parameterJson.alternatives.map((x) => x.name), - defaultValue: parameterJson.defaultValue + parameterShortName: parameter.shortName, + parameterLongName: parameter.longName, + description: parameter.description, + required: parameter.required, + alternatives: parameter.alternatives.map((x) => x.name), + defaultValue: parameter.defaultValue }); break; case 'string': customParameter = this.defineStringParameter({ - parameterLongName: parameterJson.longName, - parameterShortName: parameterJson.shortName, - description: parameterJson.description, - required: parameterJson.required, - argumentName: parameterJson.argumentName + parameterLongName: parameter.longName, + parameterShortName: parameter.shortName, + description: parameter.description, + required: parameter.required, + argumentName: parameter.argumentName }); break; default: throw new Error( `${RushConstants.commandLineFilename} defines a parameter "${ - (parameterJson as ParameterJson).longName - }" using an unsupported parameter kind "${(parameterJson as ParameterJson).parameterKind}"` + (parameter as ParameterJson).longName + }" using an unsupported parameter kind "${(parameter as ParameterJson).parameterKind}"` ); } - if (customParameter) { - this.customParameters.push(customParameter); - } + this.customParameters.push(customParameter); } } } diff --git a/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts b/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts index d7fc51fde29..2c49b9e1878 100644 --- a/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts +++ b/apps/rush-lib/src/cli/scriptActions/GlobalScriptAction.ts @@ -9,12 +9,12 @@ import { FileSystem, IPackageJson, JsonFile, AlreadyReportedError, Text } from ' import { BaseScriptAction, IBaseScriptActionOptions } from './BaseScriptAction'; import { Utilities } from '../../utilities/Utilities'; import { Autoinstaller } from '../../logic/Autoinstaller'; -import { IShellCommandTokenContext } from '../../api/CommandLineConfiguration'; +import { IGlobalCommand, IShellCommandTokenContext } from '../../api/CommandLineConfiguration'; /** * Constructor parameters for GlobalScriptAction. */ -export interface IGlobalScriptActionOptions extends IBaseScriptActionOptions { +export interface IGlobalScriptActionOptions extends IBaseScriptActionOptions { shellCommand: string; autoinstallerName: string | undefined; } @@ -29,7 +29,7 @@ export interface IGlobalScriptActionOptions extends IBaseScriptActionOptions { * and "rebuild" commands are also modeled as bulk commands, because they essentially just * invoke scripts from package.json in the same way as a custom command. */ -export class GlobalScriptAction extends BaseScriptAction { +export class GlobalScriptAction extends BaseScriptAction { private readonly _shellCommand: string; private readonly _autoinstallerName: string; private readonly _autoinstallerFullPath: string; @@ -88,7 +88,7 @@ export class GlobalScriptAction extends BaseScriptAction { public async runAsync(): Promise { const additionalPathFolders: string[] = - this._commandLineConfiguration?.additionalPathFolders.slice() || []; + this.commandLineConfiguration?.additionalPathFolders.slice() || []; if (this._autoinstallerName) { await this._prepareAutoinstallerName(); @@ -121,7 +121,7 @@ export class GlobalScriptAction extends BaseScriptAction { } const shellCommandTokenContext: IShellCommandTokenContext | undefined = - this._commandLineConfiguration?.shellCommandTokenContext; + this.commandLineConfiguration?.shellCommandTokenContext; if (shellCommandTokenContext) { shellCommand = this._expandShellCommandWithTokens(shellCommand, shellCommandTokenContext); } diff --git a/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts b/apps/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts similarity index 81% rename from apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts rename to apps/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 463f244d3e6..d1a9125008b 100644 --- a/apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts +++ b/apps/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -9,43 +9,37 @@ import { CommandLineFlagParameter, CommandLineStringParameter } from '@rushstack import { Event } from '../../index'; import { SetupChecks } from '../../logic/SetupChecks'; -import { - INonPhasedProjectTaskSelectorOptions, - NonPhasedProjectTaskSelector -} from '../../logic/NonPhasedProjectTaskSelector'; import { Stopwatch, StopwatchState } from '../../utilities/Stopwatch'; import { BaseScriptAction, IBaseScriptActionOptions } from './BaseScriptAction'; import { ITaskExecutionManagerOptions, TaskExecutionManager } from '../../logic/taskExecution/TaskExecutionManager'; -import { Utilities } from '../../utilities/Utilities'; import { RushConstants } from '../../logic/RushConstants'; import { EnvironmentVariableNames } from '../../api/EnvironmentConfiguration'; import { LastLinkFlag, LastLinkFlagFactory } from '../../api/LastLinkFlag'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; -import { Selection } from '../../logic/Selection'; import { SelectionParameterSet } from '../SelectionParameterSet'; -import { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; +import { CommandLineConfiguration, IPhase, IPhasedCommand } from '../../api/CommandLineConfiguration'; +import { IProjectTaskSelectorOptions, ProjectTaskSelector } from '../../logic/ProjectTaskSelector'; +import { IFlagParameterJson } from '../../api/CommandLineJson'; /** * Constructor parameters for BulkScriptAction. */ -export interface IBulkScriptActionOptions extends IBaseScriptActionOptions { +export interface IPhasedScriptActionOptions extends IBaseScriptActionOptions { enableParallelism: boolean; - ignoreMissingScript: boolean; - ignoreDependencyOrder: boolean; incremental: boolean; - allowWarningsInSuccessfulBuild: boolean; watchForChanges: boolean; disableBuildCache: boolean; - logFilenameIdentifier: string; - commandToRun: string; + + actionPhases: string[]; + phases: Map; } interface IExecuteInternalOptions { - taskSelectorOptions: INonPhasedProjectTaskSelectorOptions; + taskSelectorOptions: IProjectTaskSelectorOptions; taskExecutionManagerOptions: ITaskExecutionManagerOptions; stopwatch: Stopwatch; ignoreHooks?: boolean; @@ -54,24 +48,22 @@ interface IExecuteInternalOptions { /** * This class implements bulk commands which are run individually for each project in the repo, - * possibly in parallel. The action executes a script found in the project's package.json file. + * possibly in parallel. The task selector is abstract and is implemented for phased or non-phased + * commands. * * @remarks * Bulk commands can be defined via common/config/command-line.json. Rush's predefined "build" * and "rebuild" commands are also modeled as bulk commands, because they essentially just * execute scripts from package.json in the same as any custom command. */ -export class BulkScriptAction extends BaseScriptAction { +export class PhasedScriptAction extends BaseScriptAction { private readonly _enableParallelism: boolean; - private readonly _ignoreMissingScript: boolean; private readonly _isIncrementalBuildAllowed: boolean; - private readonly _commandToRun: string; private readonly _watchForChanges: boolean; private readonly _disableBuildCache: boolean; - private readonly _repoCommandLineConfiguration: CommandLineConfiguration | undefined; - private readonly _ignoreDependencyOrder: boolean; - private readonly _allowWarningsInSuccessfulBuild: boolean; - private readonly _logFilenameIdentifier: string; + private readonly _repoCommandLineConfiguration: CommandLineConfiguration; + private readonly _actionPhases: string[]; + private readonly _phases: Map; private _changedProjectsOnly!: CommandLineFlagParameter; private _selectionParameters!: SelectionParameterSet; @@ -79,18 +71,15 @@ export class BulkScriptAction extends BaseScriptAction { private _parallelismParameter: CommandLineStringParameter | undefined; private _ignoreHooksParameter!: CommandLineFlagParameter; - public constructor(options: IBulkScriptActionOptions) { + public constructor(options: IPhasedScriptActionOptions) { super(options); this._enableParallelism = options.enableParallelism; - this._ignoreMissingScript = options.ignoreMissingScript; this._isIncrementalBuildAllowed = options.incremental; - this._commandToRun = options.commandToRun; - this._ignoreDependencyOrder = options.ignoreDependencyOrder; - this._allowWarningsInSuccessfulBuild = options.allowWarningsInSuccessfulBuild; this._watchForChanges = options.watchForChanges; this._disableBuildCache = options.disableBuildCache; this._repoCommandLineConfiguration = options.commandLineConfiguration; - this._logFilenameIdentifier = options.logFilenameIdentifier; + this._actionPhases = options.actionPhases; + this._phases = options.phases; } public async runAsync(): Promise { @@ -127,7 +116,7 @@ export class BulkScriptAction extends BaseScriptAction { const terminal: Terminal = new Terminal(this.rushSession.terminalProvider); let buildCacheConfiguration: BuildCacheConfiguration | undefined; - if (!this._disableBuildCache && ['build', 'rebuild'].includes(this.actionName)) { + if (!this._disableBuildCache) { buildCacheConfiguration = await BuildCacheConfiguration.tryLoadAsync( terminal, this.rushConfiguration, @@ -135,30 +124,44 @@ export class BulkScriptAction extends BaseScriptAction { ); } - const selection: Set = await this._selectionParameters.getSelectedProjectsAsync( - terminal - ); + const projectSelection: Set = + await this._selectionParameters.getSelectedProjectsAsync(terminal); - if (!selection.size) { + if (!projectSelection.size) { terminal.writeLine(colors.yellow(`The command line selection parameters did not match any projects.`)); return; } - const taskSelectorOptions: INonPhasedProjectTaskSelectorOptions = { + const phasesToRun: Set = new Set(this._actionPhases); + for (const parameter of this.commandLineConfiguration.parameters) { + if (parameter.parameterKind === 'flag') { + if (this.getFlagParameter(parameter.longName)!.value) { + const flagParameter: IFlagParameterJson = parameter as IFlagParameterJson; + if (flagParameter.addPhasesToCommand) { + for (const phase of flagParameter.addPhasesToCommand) { + phasesToRun.add(phase); + } + } + + if (flagParameter.skipPhasesForCommand) { + for (const phase of flagParameter.skipPhasesForCommand) { + phasesToRun.delete(phase); + } + } + } + } + } + + const taskSelectorOptions: IProjectTaskSelectorOptions = { rushConfiguration: this.rushConfiguration, buildCacheConfiguration, - selection, - commandName: this.actionName, - commandToRun: this._commandToRun, + projectSelection, customParameterValues, isQuietMode: isQuietMode, isDebugMode: isDebugMode, isIncrementalBuildAllowed: this._isIncrementalBuildAllowed, - ignoreMissingScript: this._ignoreMissingScript, - ignoreDependencyOrder: this._ignoreDependencyOrder, - allowWarningsInSuccessfulBuild: this._allowWarningsInSuccessfulBuild, - packageDepsFilename: Utilities.getPackageDepsFilenameForCommand(this._commandToRun), - logFilenameIdentifier: this._logFilenameIdentifier + phasesToRun: phasesToRun, + phases: this._phases }; const taskExecutionManagerOptions: ITaskExecutionManagerOptions = { @@ -166,7 +169,6 @@ export class BulkScriptAction extends BaseScriptAction { debugMode: this.parser.isDebug, parallelism: parallelism, changedProjectsOnly: changedProjectsOnly, - allowWarningsInSuccessfulExecution: this._allowWarningsInSuccessfulBuild, repoCommandLineConfiguration: this._repoCommandLineConfiguration }; @@ -193,7 +195,7 @@ export class BulkScriptAction extends BaseScriptAction { */ private async _runWatch(options: IExecuteInternalOptions): Promise { const { - taskSelectorOptions: { selection: projectsToWatch }, + taskSelectorOptions: { projectSelection: projectsToWatch }, stopwatch, terminal } = options; @@ -223,31 +225,25 @@ export class BulkScriptAction extends BaseScriptAction { // On the initial invocation, this promise will return immediately with the full set of projects const { changedProjects, state } = await projectWatcher.waitForChange(onWatchingFiles); - let selection: ReadonlySet = changedProjects; - if (stopwatch.state === StopwatchState.Stopped) { // Clear and reset the stopwatch so that we only report time from a single execution at a time stopwatch.reset(); stopwatch.start(); } - terminal.writeLine(`Detected changes in ${selection.size} project${selection.size === 1 ? '' : 's'}:`); - const names: string[] = [...selection].map((x) => x.packageName).sort(); + terminal.writeLine( + `Detected changes in ${changedProjects.size} project${changedProjects.size === 1 ? '' : 's'}:` + ); + const names: string[] = [...changedProjects].map((x) => x.packageName).sort(); for (const name of names) { terminal.writeLine(` ${colors.cyan(name)}`); } - // If the command ignores dependency order, that means that only the changed projects should be affected - // That said, running watch for commands that ignore dependency order may have unexpected results - if (!this._ignoreDependencyOrder) { - selection = Selection.intersection(Selection.expandAllConsumers(selection), projectsToWatch); - } - const executeOptions: IExecuteInternalOptions = { taskSelectorOptions: { ...options.taskSelectorOptions, // Revise down the set of projects to execute the command on - selection, + projectSelection: changedProjects, // Pass the ProjectChangeAnalyzer from the state differ to save a bit of overhead projectChangeAnalyzer: state }, @@ -324,9 +320,7 @@ export class BulkScriptAction extends BaseScriptAction { * Runs a single invocation of the command */ private async _runOnce(options: IExecuteInternalOptions): Promise { - const taskSelector: NonPhasedProjectTaskSelector = new NonPhasedProjectTaskSelector( - options.taskSelectorOptions - ); + const taskSelector: ProjectTaskSelector = new ProjectTaskSelector(options.taskSelectorOptions); // Register all tasks with the task collection diff --git a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap index 1fb2d972550..552a1297f07 100644 --- a/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap +++ b/apps/rush-lib/src/cli/test/__snapshots__/CommandLineHelp.test.ts.snap @@ -54,13 +54,11 @@ Positional arguments: (EXPERIMENTAL) Update the credentials used by the build cache provider. version Manage package versions in the repo. - write-build-cache Writes the current state of the current project to - the cache. import-strings Imports translated strings into each project. upload Uploads the built files to the server build Build all projects that haven't been built, or have changed since they were last built. - rebuild Clean and rebuild the entire set of projects + rebuild Clean and rebuild the entire set of projects. tab-complete Provides tab completion. Optional arguments: @@ -1235,18 +1233,3 @@ Optional arguments: what you are skipping. " `; - -exports[`CommandLineHelp prints the help for each action: write-build-cache 1`] = ` -"usage: rush write-build-cache [-h] -c COMMAND [-v] - -(EXPERIMENTAL) If the build cache is configured, when this command is run in -the folder of a project, write the current state of the project to the cache. - -Optional arguments: - -h, --help Show this help message and exit. - -c COMMAND, --command COMMAND - (Required) The command run in the current project - that produced the current project state. - -v, --verbose Display verbose log information. -" -`; diff --git a/apps/rush-lib/src/logic/NonPhasedProjectTaskSelector.ts b/apps/rush-lib/src/logic/NonPhasedProjectTaskSelector.ts deleted file mode 100644 index 743e059109c..00000000000 --- a/apps/rush-lib/src/logic/NonPhasedProjectTaskSelector.ts +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { ITaskSelectorOptions, TaskSelectorBase } from './TaskSelectorBase'; -import { RushConfigurationProject } from '../api/RushConfigurationProject'; -import { TaskCollection } from './taskExecution/TaskCollection'; -import { ProjectTaskRunner } from './taskExecution/ProjectTaskRunner'; - -export interface INonPhasedProjectTaskSelectorOptions extends ITaskSelectorOptions { - logFilenameIdentifier: string; -} - -export class NonPhasedProjectTaskSelector extends TaskSelectorBase { - private readonly _logFilenameIdentifier: string; - - public constructor(options: INonPhasedProjectTaskSelectorOptions) { - super(options); - this._logFilenameIdentifier = options.logFilenameIdentifier; - } - - public registerTasks(): TaskCollection { - const projects: ReadonlySet = this._options.selection; - const taskCollection: TaskCollection = new TaskCollection(); - - // Register all tasks - for (const rushProject of projects) { - this._registerProjectTask(rushProject, taskCollection); - } - - if (!this._options.ignoreDependencyOrder) { - const dependencyMap: Map> = new Map(); - - // Generate the filtered dependency graph for selected projects - function getDependencyTaskNames(project: RushConfigurationProject): Set { - const cached: Set | undefined = dependencyMap.get(project); - if (cached) { - return cached; - } - - const dependencyTaskNames: Set = new Set(); - dependencyMap.set(project, dependencyTaskNames); - - for (const dep of project.dependencyProjects) { - if (projects.has(dep)) { - // Add direct relationships for projects in the set - dependencyTaskNames.add(NonPhasedProjectTaskSelector.getTaskNameForProject(dep)); - } else { - // Add indirect relationships for projects not in the set - for (const indirectDep of getDependencyTaskNames(dep)) { - dependencyTaskNames.add(indirectDep); - } - } - } - - return dependencyTaskNames; - } - - // Add ordering relationships for each dependency - for (const project of projects) { - taskCollection.addDependencies( - NonPhasedProjectTaskSelector.getTaskNameForProject(project), - getDependencyTaskNames(project) - ); - } - } - - return taskCollection; - } - - private _registerProjectTask(project: RushConfigurationProject, taskCollection: TaskCollection): void { - const taskName: string = NonPhasedProjectTaskSelector.getTaskNameForProject(project); - if (taskCollection.hasTask(taskName)) { - return; - } - - const commandToRun: string | undefined = TaskSelectorBase.getScriptToRun( - project, - this._options.commandToRun, - this._options.customParameterValues - ); - if (commandToRun === undefined && !this._options.ignoreMissingScript) { - throw new Error( - `The project [${project.packageName}] does not define a '${this._options.commandToRun}' command in the 'scripts' section of its package.json` - ); - } - - taskCollection.addTask( - new ProjectTaskRunner({ - rushProject: project, - taskName, - rushConfiguration: this._options.rushConfiguration, - buildCacheConfiguration: this._options.buildCacheConfiguration, - commandToRun: commandToRun || '', - commandName: this._options.commandName, - isIncrementalBuildAllowed: this._options.isIncrementalBuildAllowed, - projectChangeAnalyzer: this._projectChangeAnalyzer, - packageDepsFilename: this._options.packageDepsFilename, - allowWarningsInSuccessfulBuild: this._options.allowWarningsInSuccessfulBuild, - logFilenameIdentifier: this._logFilenameIdentifier - }) - ); - } - - /** - * A helper method to determine the task name of a ProjectBuilder. Used when the task - * name is required before a task is created. - */ - public static getTaskNameForProject(rushProject: RushConfigurationProject): string { - return rushProject.packageName; - } -} diff --git a/apps/rush-lib/src/logic/ProjectChangeAnalyzer.ts b/apps/rush-lib/src/logic/ProjectChangeAnalyzer.ts index 6f76365bc70..08e623a7015 100644 --- a/apps/rush-lib/src/logic/ProjectChangeAnalyzer.ts +++ b/apps/rush-lib/src/logic/ProjectChangeAnalyzer.ts @@ -22,6 +22,7 @@ import { RushConfigurationProject } from '../api/RushConfigurationProject'; import { RushConstants } from './RushConstants'; import { LookupByPath } from './LookupByPath'; import { PnpmShrinkwrapFile } from './pnpm/PnpmShrinkwrapFile'; +import { UNINITIALIZED } from '../utilities/Utilities'; /** * @beta @@ -63,10 +64,10 @@ export class ProjectChangeAnalyzer { * UNINITIALIZED === we haven't looked * undefined === data isn't available (i.e. - git isn't present) */ - private _data: IRawRepoState | undefined = undefined; - private _filteredData: Map> = new Map(); - private _projectStateCache: Map = new Map(); - private _rushConfiguration: RushConfiguration; + private _data: IRawRepoState | UNINITIALIZED | undefined = UNINITIALIZED; + private readonly _filteredData: Map> = new Map(); + private readonly _projectStateCache: Map = new Map(); + private readonly _rushConfiguration: RushConfiguration; private readonly _git: Git; public constructor(rushConfiguration: RushConfiguration) { @@ -92,10 +93,14 @@ export class ProjectChangeAnalyzer { return filteredProjectData; } - if (this._data === undefined) { + if (this._data === UNINITIALIZED) { this._data = this._getData(terminal); } + if (!this._data) { + return undefined; + } + const { projectState, rootDir } = this._data; if (projectState === undefined) { @@ -410,10 +415,8 @@ export class ProjectChangeAnalyzer { project: RushConfigurationProject, terminal: ITerminal ): Promise { - const projectConfiguration: RushProjectConfiguration | undefined = - await RushProjectConfiguration.tryLoadForProjectAsync(project, undefined, terminal); - - const { incrementalBuildIgnoredGlobs } = projectConfiguration || {}; + const incrementalBuildIgnoredGlobs: ReadonlyArray | undefined = + await RushProjectConfiguration.tryLoadIgnoreGlobsForProjectAsync(project, terminal); if (incrementalBuildIgnoredGlobs && incrementalBuildIgnoredGlobs.length) { const ignoreMatcher: Ignore = ignore(); diff --git a/apps/rush-lib/src/logic/ProjectTaskSelector.ts b/apps/rush-lib/src/logic/ProjectTaskSelector.ts new file mode 100644 index 00000000000..32cccdf9af9 --- /dev/null +++ b/apps/rush-lib/src/logic/ProjectTaskSelector.ts @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { BuildCacheConfiguration } from '../api/BuildCacheConfiguration'; +import { RushConfiguration } from '../api/RushConfiguration'; +import { RushConfigurationProject } from '../api/RushConfigurationProject'; +import { convertSlashesForWindows, ProjectTaskRunner } from './taskExecution/ProjectTaskRunner'; +import { ProjectChangeAnalyzer } from './ProjectChangeAnalyzer'; +import { TaskCollection } from './taskExecution/TaskCollection'; +import { IPhase } from '../api/CommandLineConfiguration'; +import { RushConstants } from './RushConstants'; + +export interface IProjectTaskSelectorOptions { + rushConfiguration: RushConfiguration; + buildCacheConfiguration: BuildCacheConfiguration | undefined; + projectSelection: ReadonlySet; + customParameterValues: string[]; + isQuietMode: boolean; + isDebugMode: boolean; + isIncrementalBuildAllowed: boolean; + projectChangeAnalyzer?: ProjectChangeAnalyzer; + + phasesToRun: Iterable; + phases: Map; +} + +/** + * This class is responsible for: + * - based on to/from flags, solving the dependency graph and figuring out which projects need to be run + * - creating a ProjectBuilder for each project that needs to be built + * - registering the necessary ProjectBuilders with the TaskExecutionManager, which actually orchestrates execution + */ +export class ProjectTaskSelector { + private readonly _options: IProjectTaskSelectorOptions; + private readonly _projectChangeAnalyzer: ProjectChangeAnalyzer; + private readonly _phasesToRun: Iterable; + private readonly _phases: Map; + + public constructor(options: IProjectTaskSelectorOptions) { + this._options = options; + this._projectChangeAnalyzer = + options.projectChangeAnalyzer || new ProjectChangeAnalyzer(options.rushConfiguration); + this._phasesToRun = options.phasesToRun; + this._phases = options.phases; + } + + public static getScriptToRun( + rushProject: RushConfigurationProject, + commandToRun: string, + customParameterValues: string[] + ): string | undefined { + const script: string | undefined = ProjectTaskSelector._getScriptCommand(rushProject, commandToRun); + + if (script === undefined) { + return undefined; + } + + if (!script) { + return ''; + } else { + const taskCommand: string = `${script} ${customParameterValues.join(' ')}`; + return process.platform === 'win32' ? convertSlashesForWindows(taskCommand) : taskCommand; + } + } + + public registerTasks(): TaskCollection { + const projects: ReadonlySet = this._options.projectSelection; + const taskCollection: TaskCollection = new TaskCollection(); + + // Register all tasks + for (const phaseName of this._phasesToRun) { + const phase: IPhase = this._getPhaseByName(phaseName); + for (const rushProject of projects) { + this._registerProjectPhaseTask(rushProject, phase, taskCollection); + } + } + + return taskCollection; + } + + private _registerProjectPhaseTask( + project: RushConfigurationProject, + phase: IPhase, + taskCollection: TaskCollection + ): string { + const taskName: string = this._getPhaseDisplayNameForProject(phase, project); + if (taskCollection.hasTask(taskName)) { + return taskName; + } + + const commandToRun: string | undefined = ProjectTaskSelector.getScriptToRun( + project, + phase.name, + this._options.customParameterValues + ); + if (commandToRun === undefined && !phase.ignoreMissingScript) { + throw new Error( + `The project [${project.packageName}] does not define a '${phase.name}' command in the 'scripts' section of its package.json` + ); + } + + taskCollection.addTask( + new ProjectTaskRunner({ + rushProject: project, + taskName, + rushConfiguration: this._options.rushConfiguration, + buildCacheConfiguration: this._options.buildCacheConfiguration, + commandToRun: commandToRun || '', + isIncrementalBuildAllowed: this._options.isIncrementalBuildAllowed, + projectChangeAnalyzer: this._projectChangeAnalyzer, + phase + }) + ); + + const dependencyTasks: Set = new Set(); + if (phase.dependencies?.self) { + for (const dependencyPhaseName of phase.dependencies.self) { + const dependencyPhase: IPhase = this._getPhaseByName(dependencyPhaseName); + const dependencyTaskName: string = this._registerProjectPhaseTask( + project, + dependencyPhase, + taskCollection + ); + + dependencyTasks.add(dependencyTaskName); + } + } + + if (phase.dependencies?.upstream) { + for (const dependencyPhaseName of phase.dependencies.upstream) { + const dependencyPhase: IPhase = this._getPhaseByName(dependencyPhaseName); + for (const dependencyProject of project.dependencyProjects) { + const dependencyTaskName: string = this._registerProjectPhaseTask( + dependencyProject, + dependencyPhase, + taskCollection + ); + + dependencyTasks.add(dependencyTaskName); + } + } + } + + taskCollection.addDependencies(taskName, dependencyTasks); + + return taskName; + } + + private _getPhaseByName(phaseName: string): IPhase { + const phase: IPhase | undefined = this._phases.get(phaseName); + if (!phase) { + throw new Error(`Phase ${phaseName} not found`); + } + + return phase; + } + + private _getPhaseDisplayNameForProject(phase: IPhase, project: RushConfigurationProject): string { + if (phase.isSynthetic) { + // Because this is a synthetic phase, just use the project name because there aren't any other phases + return project.packageName; + } else { + const phaseNameWithoutPrefix: string = phase.name.substring(RushConstants.phaseNamePrefix.length); + return `${project.packageName} (${phaseNameWithoutPrefix})`; + } + } + + private static _getScriptCommand( + rushProject: RushConfigurationProject, + script: string + ): string | undefined { + if (!rushProject.packageJson.scripts) { + return undefined; + } + + const rawCommand: string = rushProject.packageJson.scripts[script]; + + if (rawCommand === undefined || rawCommand === null) { + return undefined; + } + + return rawCommand; + } +} diff --git a/apps/rush-lib/src/logic/RushConstants.ts b/apps/rush-lib/src/logic/RushConstants.ts index 8d7c8269459..3ea528951c2 100644 --- a/apps/rush-lib/src/logic/RushConstants.ts +++ b/apps/rush-lib/src/logic/RushConstants.ts @@ -234,4 +234,9 @@ export class RushConstants { * The name of the project `rush-logs` folder. */ public static readonly rushLogsFolderName: string = 'rush-logs'; + + /** + * The expected prefix for phase names in "common/config/rush/command-line.json" + */ + public static readonly phaseNamePrefix: '_phase:' = '_phase:'; } diff --git a/apps/rush-lib/src/logic/TaskSelectorBase.ts b/apps/rush-lib/src/logic/TaskSelectorBase.ts deleted file mode 100644 index 707533c8dc9..00000000000 --- a/apps/rush-lib/src/logic/TaskSelectorBase.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { BuildCacheConfiguration } from '../api/BuildCacheConfiguration'; -import { RushConfiguration } from '../api/RushConfiguration'; -import { RushConfigurationProject } from '../api/RushConfigurationProject'; -import { convertSlashesForWindows } from './taskExecution/ProjectTaskRunner'; -import { ProjectChangeAnalyzer } from './ProjectChangeAnalyzer'; -import { TaskCollection } from './taskExecution/TaskCollection'; - -export interface ITaskSelectorOptions { - rushConfiguration: RushConfiguration; - buildCacheConfiguration: BuildCacheConfiguration | undefined; - selection: ReadonlySet; - commandName: string; - commandToRun: string; - customParameterValues: string[]; - isQuietMode: boolean; - isDebugMode: boolean; - isIncrementalBuildAllowed: boolean; - ignoreMissingScript: boolean; - ignoreDependencyOrder: boolean; - packageDepsFilename: string; - projectChangeAnalyzer?: ProjectChangeAnalyzer; - allowWarningsInSuccessfulBuild?: boolean; -} - -/** - * This class is responsible for: - * - based on to/from flags, solving the dependency graph and figuring out which projects need to be run - * - creating a ProjectBuilder for each project that needs to be built - * - registering the necessary ProjectBuilders with the TaskExecutionManager, which actually orchestrates execution - */ -export abstract class TaskSelectorBase { - protected _options: ITaskSelectorOptions; - protected _projectChangeAnalyzer: ProjectChangeAnalyzer; - - public constructor(options: ITaskSelectorOptions) { - this._options = options; - - const { projectChangeAnalyzer = new ProjectChangeAnalyzer(options.rushConfiguration) } = options; - - this._projectChangeAnalyzer = projectChangeAnalyzer; - } - - public static getScriptToRun( - rushProject: RushConfigurationProject, - commandToRun: string, - customParameterValues: string[] - ): string | undefined { - const script: string | undefined = TaskSelectorBase._getScriptCommand(rushProject, commandToRun); - - if (script === undefined) { - return undefined; - } - - if (!script) { - return ''; - } else { - const taskCommand: string = `${script} ${customParameterValues.join(' ')}`; - return process.platform === 'win32' ? convertSlashesForWindows(taskCommand) : taskCommand; - } - } - - public abstract registerTasks(): TaskCollection; - - private static _getScriptCommand( - rushProject: RushConfigurationProject, - script: string - ): string | undefined { - if (!rushProject.packageJson.scripts) { - return undefined; - } - - const rawCommand: string = rushProject.packageJson.scripts[script]; - - if (rawCommand === undefined || rawCommand === null) { - return undefined; - } - - return rawCommand; - } -} diff --git a/apps/rush-lib/src/logic/buildCache/CacheEntryId.ts b/apps/rush-lib/src/logic/buildCache/CacheEntryId.ts index 04b994cf9a9..130ee095b77 100644 --- a/apps/rush-lib/src/logic/buildCache/CacheEntryId.ts +++ b/apps/rush-lib/src/logic/buildCache/CacheEntryId.ts @@ -5,6 +5,7 @@ const OPTIONS_ARGUMENT_NAME: string = 'options'; export interface IGenerateCacheEntryIdOptions { projectName: string; + phaseName: string; projectStateHash: string; } @@ -12,6 +13,7 @@ export type GetCacheEntryIdFunction = (options: IGenerateCacheEntryIdOptions) => const HASH_TOKEN_NAME: string = 'hash'; const PROJECT_NAME_TOKEN_NAME: string = 'projectName'; +const PHASE_NAME_TOKEN_NAME: string = 'phaseName'; // This regex matches substrings that look like [token] const TOKEN_REGEX: RegExp = /\[[^\]]*\]/g; @@ -84,6 +86,31 @@ export class CacheEntryId { } } + case PHASE_NAME_TOKEN_NAME: { + switch (tokenAttribute) { + case undefined: { + throw new Error( + 'Either the "normalize" or the "trimPrefix" attribute is required ' + + `for the "${tokenName}" token.` + ); + } + + case 'normalize': { + // Replace colons with underscores. + return `\${${OPTIONS_ARGUMENT_NAME}.phaseName.replace(/:/g, '_')}`; + } + + case 'trimPrefix': { + // Trim the "_phase:" prefix from the phase name. + return `\${${OPTIONS_ARGUMENT_NAME}.phaseName.replace(/^_phase:/, '')}`; + } + + default: { + throw new Error(`Unexpected attribute "${tokenAttribute}" for the "${tokenName}" token.`); + } + } + } + default: { throw new Error(`Unexpected token name "${tokenName}".`); } diff --git a/apps/rush-lib/src/logic/buildCache/ProjectBuildCache.ts b/apps/rush-lib/src/logic/buildCache/ProjectBuildCache.ts index 45503d32140..d132be7a5b4 100644 --- a/apps/rush-lib/src/logic/buildCache/ProjectBuildCache.ts +++ b/apps/rush-lib/src/logic/buildCache/ProjectBuildCache.ts @@ -22,10 +22,12 @@ import { Utilities } from '../../utilities/Utilities'; export interface IProjectBuildCacheOptions { buildCacheConfiguration: BuildCacheConfiguration; projectConfiguration: RushProjectConfiguration; + projectOutputFolderNames: ReadonlyArray; command: string; trackedProjectFiles: string[] | undefined; projectChangeAnalyzer: ProjectChangeAnalyzer; terminal: ITerminal; + phaseName: string; } interface IPathsToCache { @@ -52,7 +54,7 @@ export class ProjectBuildCache { this._localBuildCacheProvider = options.buildCacheConfiguration.localCacheProvider; this._cloudBuildCacheProvider = options.buildCacheConfiguration.cloudCacheProvider; this._buildCacheEnabled = options.buildCacheConfiguration.buildCacheEnabled; - this._projectOutputFolderNames = options.projectConfiguration.projectOutputFolderNames || []; + this._projectOutputFolderNames = options.projectOutputFolderNames || []; this._cacheId = cacheId; } @@ -67,12 +69,19 @@ export class ProjectBuildCache { public static async tryGetProjectBuildCache( options: IProjectBuildCacheOptions ): Promise { - const { terminal, projectConfiguration, trackedProjectFiles } = options; + const { terminal, projectConfiguration, projectOutputFolderNames, trackedProjectFiles } = options; if (!trackedProjectFiles) { return undefined; } - if (!ProjectBuildCache._validateProject(terminal, projectConfiguration, trackedProjectFiles)) { + if ( + !ProjectBuildCache._validateProject( + terminal, + projectConfiguration, + projectOutputFolderNames, + trackedProjectFiles + ) + ) { return undefined; } @@ -83,14 +92,15 @@ export class ProjectBuildCache { private static _validateProject( terminal: ITerminal, projectConfiguration: RushProjectConfiguration, + projectOutputFolderNames: ReadonlyArray, trackedProjectFiles: string[] ): boolean { const normalizedProjectRelativeFolder: string = Path.convertToSlashes( projectConfiguration.project.projectRelativeFolder ); const outputFolders: string[] = []; - if (projectConfiguration.projectOutputFolderNames) { - for (const outputFolderName of projectConfiguration.projectOutputFolderNames) { + if (projectOutputFolderNames) { + for (const outputFolderName of projectOutputFolderNames) { outputFolders.push(`${normalizedProjectRelativeFolder}/${outputFolderName}/`); } } @@ -470,9 +480,7 @@ export class ProjectBuildCache { const sortedProjectStates: string[] = projectStates.sort(); const hash: crypto.Hash = crypto.createHash('sha1'); - const serializedOutputFolders: string = JSON.stringify( - options.projectConfiguration.projectOutputFolderNames - ); + const serializedOutputFolders: string = JSON.stringify(options.projectOutputFolderNames); hash.update(serializedOutputFolders); hash.update(RushConstants.hashDelimiter); hash.update(options.command); @@ -486,7 +494,8 @@ export class ProjectBuildCache { return options.buildCacheConfiguration.getCacheEntryId({ projectName: options.projectConfiguration.project.packageName, - projectStateHash + projectStateHash, + phaseName: options.phaseName }); } } diff --git a/apps/rush-lib/src/logic/buildCache/test/CacheEntryId.test.ts b/apps/rush-lib/src/logic/buildCache/test/CacheEntryId.test.ts index 05ebd5c7044..c05247f5c56 100644 --- a/apps/rush-lib/src/logic/buildCache/test/CacheEntryId.test.ts +++ b/apps/rush-lib/src/logic/buildCache/test/CacheEntryId.test.ts @@ -1,36 +1,62 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { CacheEntryId, GetCacheEntryIdFunction } from '../CacheEntryId'; +import { CacheEntryId, GetCacheEntryIdFunction, IGenerateCacheEntryIdOptions } from '../CacheEntryId'; describe(CacheEntryId.name, () => { describe('Valid pattern names', () => { - function validatePatternMatchesSnapshot(projectName: string, pattern?: string): void { + function validatePatternMatchesSnapshot( + projectName: string, + pattern?: string, + generateCacheEntryIdOptions?: Partial + ): void { const getCacheEntryId: GetCacheEntryIdFunction = CacheEntryId.parsePattern(pattern); expect( getCacheEntryId({ projectName, - projectStateHash: '09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3' + projectStateHash: '09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3', + phaseName: '_phase:compile', + ...generateCacheEntryIdOptions }) - ).toMatchSnapshot(); + ).toMatchSnapshot(pattern || 'no pattern'); } + // prettier-ignore it('Handles a cache entry name for a project name without a scope', () => { const projectName: string = 'project+name'; validatePatternMatchesSnapshot(projectName); validatePatternMatchesSnapshot(projectName, '[hash]'); validatePatternMatchesSnapshot(projectName, '[projectName]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[phaseName:trimPrefix]_[hash]'); validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[phaseName:trimPrefix]_[hash]'); validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[phaseName:trimPrefix]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[phaseName:trimPrefix]_[hash]'); }); + // prettier-ignore it('Handles a cache entry name for a project name with a scope', () => { const projectName: string = '@scope/project+name'; validatePatternMatchesSnapshot(projectName); validatePatternMatchesSnapshot(projectName, '[hash]'); validatePatternMatchesSnapshot(projectName, '[projectName]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[phaseName:trimPrefix]_[hash]'); validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, '[projectName:normalize]_[phaseName:trimPrefix]_[hash]'); validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName:normalize]_[phaseName:trimPrefix]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[phaseName:normalize]_[hash]'); + validatePatternMatchesSnapshot(projectName, 'prefix/[projectName]_[phaseName:trimPrefix]_[hash]'); }); }); @@ -48,6 +74,9 @@ describe(CacheEntryId.name, () => { await validateInvalidPatternErrorMatchesSnapshotAsync('[hash:badAttribute:attr2]'); await validateInvalidPatternErrorMatchesSnapshotAsync('[projectName:badAttribute]'); await validateInvalidPatternErrorMatchesSnapshotAsync('[projectName:]'); + await validateInvalidPatternErrorMatchesSnapshotAsync('[phaseName]'); + await validateInvalidPatternErrorMatchesSnapshotAsync('[phaseName:]'); + await validateInvalidPatternErrorMatchesSnapshotAsync('[phaseName:badAttribute]'); await validateInvalidPatternErrorMatchesSnapshotAsync('[:attr1]'); await validateInvalidPatternErrorMatchesSnapshotAsync('[projectName:attr1:attr2]'); await validateInvalidPatternErrorMatchesSnapshotAsync('/[hash]'); diff --git a/apps/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts b/apps/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts index 28897961b0b..c1d79d46507 100644 --- a/apps/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts +++ b/apps/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts @@ -35,8 +35,8 @@ describe('ProjectBuildCache', () => { isCacheWriteAllowed: options.hasOwnProperty('writeAllowed') ? options.writeAllowed : false } } as unknown as BuildCacheConfiguration, + projectOutputFolderNames: ['dist'], projectConfiguration: { - projectOutputFolderNames: ['dist'], project: { packageName: 'acme-wizard', projectRelativeFolder: 'apps/acme-wizard', @@ -46,7 +46,8 @@ describe('ProjectBuildCache', () => { command: 'build', trackedProjectFiles: options.hasOwnProperty('trackedProjectFiles') ? options.trackedProjectFiles : [], projectChangeAnalyzer, - terminal + terminal, + phaseName: 'build' }); return subject; diff --git a/apps/rush-lib/src/logic/buildCache/test/__snapshots__/CacheEntryId.test.ts.snap b/apps/rush-lib/src/logic/buildCache/test/__snapshots__/CacheEntryId.test.ts.snap index bd4c527b1eb..899f088afe0 100644 --- a/apps/rush-lib/src/logic/buildCache/test/__snapshots__/CacheEntryId.test.ts.snap +++ b/apps/rush-lib/src/logic/buildCache/test/__snapshots__/CacheEntryId.test.ts.snap @@ -16,30 +16,72 @@ exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid p exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 8`] = `"Unexpected attribute \\"\\" for the \\"projectName\\" token."`; -exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 9`] = `"Unexpected token name \\"\\"."`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 9`] = `"Either the \\"normalize\\" or the \\"trimPrefix\\" attribute is required for the \\"phaseName\\" token."`; -exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 10`] = `"Unexpected attribute \\"attr1:attr2\\" for the \\"projectName\\" token."`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 10`] = `"Unexpected attribute \\"\\" for the \\"phaseName\\" token."`; -exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 11`] = `"Cache entry name patterns may not start with a slash."`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 11`] = `"Unexpected attribute \\"badAttribute\\" for the \\"phaseName\\" token."`; -exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 12`] = `"Cache entry name pattern contains an invalid character. Only alphanumeric characters, slashes, underscores, and hyphens are allowed."`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 12`] = `"Unexpected token name \\"\\"."`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 13`] = `"Unexpected attribute \\"attr1:attr2\\" for the \\"projectName\\" token."`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope 2`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 14`] = `"Cache entry name patterns may not start with a slash."`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope 3`] = `"@scope/project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Invalid pattern names Throws an exception for an invalid pattern 15`] = `"Cache entry name pattern contains an invalid character. Only alphanumeric characters, slashes, underscores, and hyphens are allowed."`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope 4`] = `"@scope+project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [hash] 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope 5`] = `"prefix/@scope+project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [phaseName:normalize]_[hash] 1`] = `"_phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [phaseName:trimPrefix]_[hash] 1`] = `"compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope 2`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [projectName:normalize]_[hash] 1`] = `"@scope+project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope 3`] = `"project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [projectName:normalize]_[phaseName:normalize]_[hash] 1`] = `"@scope+project++name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope 4`] = `"project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [projectName:normalize]_[phaseName:trimPrefix]_[hash] 1`] = `"@scope+project++name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; -exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope 5`] = `"prefix/project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: [projectName]_[hash] 1`] = `"@scope/project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: no pattern 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName:normalize]_[hash] 1`] = `"prefix/@scope+project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName:normalize]_[phaseName:normalize]_[hash] 1`] = `"prefix/@scope+project++name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName:normalize]_[phaseName:trimPrefix]_[hash] 1`] = `"prefix/@scope+project++name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName]_[hash] 1`] = `"prefix/@scope/project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName]_[phaseName:normalize]_[hash] 1`] = `"prefix/@scope/project+name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name with a scope: prefix/[projectName]_[phaseName:trimPrefix]_[hash] 1`] = `"prefix/@scope/project+name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [hash] 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [phaseName:normalize]_[hash] 1`] = `"_phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [phaseName:trimPrefix]_[hash] 1`] = `"compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [projectName:normalize]_[hash] 1`] = `"project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [projectName:normalize]_[phaseName:normalize]_[hash] 1`] = `"project++name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [projectName:normalize]_[phaseName:trimPrefix]_[hash] 1`] = `"project++name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: [projectName]_[hash] 1`] = `"project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: no pattern 1`] = `"09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName:normalize]_[hash] 1`] = `"prefix/project++name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName:normalize]_[phaseName:normalize]_[hash] 1`] = `"prefix/project++name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName:normalize]_[phaseName:trimPrefix]_[hash] 1`] = `"prefix/project++name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName]_[hash] 1`] = `"prefix/project+name_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName]_[phaseName:normalize]_[hash] 1`] = `"prefix/project+name__phase_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; + +exports[`CacheEntryId Valid pattern names Handles a cache entry name for a project name without a scope: prefix/[projectName]_[phaseName:trimPrefix]_[hash] 1`] = `"prefix/project+name_compile_09d1ecee6d5f888fa6c35ca804b5dac7c3735ce3"`; diff --git a/apps/rush-lib/src/logic/taskExecution/BaseTaskRunner.ts b/apps/rush-lib/src/logic/taskExecution/BaseTaskRunner.ts index 99b44ffc760..e89ec822230 100644 --- a/apps/rush-lib/src/logic/taskExecution/BaseTaskRunner.ts +++ b/apps/rush-lib/src/logic/taskExecution/BaseTaskRunner.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { StdioSummarizer } from '@rushstack/terminal'; -import { CollatedWriter } from '@rushstack/stream-collator'; +import type { StdioSummarizer } from '@rushstack/terminal'; +import type { CollatedWriter } from '@rushstack/stream-collator'; -import { TaskStatus } from './TaskStatus'; -import { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; +import type { TaskStatus } from './TaskStatus'; +import type { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; export interface ITaskRunnerContext { - repoCommandLineConfiguration: CommandLineConfiguration | undefined; + repoCommandLineConfiguration: CommandLineConfiguration; collatedWriter: CollatedWriter; stdioSummarizer: StdioSummarizer; quietMode: boolean; @@ -37,6 +37,12 @@ export abstract class BaseTaskRunner { */ public abstract hadEmptyScript: boolean; + /** + * If set to true, a warning result should not make Rush exit with a nonzero + * exit code + */ + public abstract warningsAreAllowed: boolean; + /** * Method to be executed for the task. */ diff --git a/apps/rush-lib/src/logic/taskExecution/ProjectLogWritable.ts b/apps/rush-lib/src/logic/taskExecution/ProjectLogWritable.ts index 31cb4004a08..0310f0c6269 100644 --- a/apps/rush-lib/src/logic/taskExecution/ProjectLogWritable.ts +++ b/apps/rush-lib/src/logic/taskExecution/ProjectLogWritable.ts @@ -45,8 +45,8 @@ export class ProjectLogWritable extends TerminalWritable { projectFolder, 'build' ); - // If the multi-phase commands experiment is enabled, put logs under `rush-logs` - if (project.rushConfiguration.experimentsConfiguration.configuration._multiPhaseCommands) { + // If the phased commands experiment is enabled, put logs under `rush-logs` + if (project.rushConfiguration.experimentsConfiguration.configuration.phasedCommands) { // Delete the legacy logs FileSystem.deleteFile(legacyLogPath); FileSystem.deleteFile(legacyErrorLogPath); @@ -68,10 +68,6 @@ export class ProjectLogWritable extends TerminalWritable { this._logWriter = FileWriter.open(this._logPath); } - public static normalizeNameForLogFilenameIdentifiers(name: string): string { - return name.replace(/:/g, '_'); // Replace colons with underscores to be filesystem-safe - } - protected onWriteChunk(chunk: ITerminalChunk): void { if (!this._logWriter) { throw new InternalError('Output file was closed'); diff --git a/apps/rush-lib/src/logic/taskExecution/ProjectTaskRunner.ts b/apps/rush-lib/src/logic/taskExecution/ProjectTaskRunner.ts index 32ffe88c847..3a5e71bb357 100644 --- a/apps/rush-lib/src/logic/taskExecution/ProjectTaskRunner.ts +++ b/apps/rush-lib/src/logic/taskExecution/ProjectTaskRunner.ts @@ -24,20 +24,21 @@ import { } from '@rushstack/terminal'; import { CollatedTerminal } from '@rushstack/stream-collator'; -import { RushConfiguration } from '../../api/RushConfiguration'; -import { RushConfigurationProject } from '../../api/RushConfigurationProject'; +import type { RushConfiguration } from '../../api/RushConfiguration'; +import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { Utilities, UNINITIALIZED } from '../../utilities/Utilities'; import { TaskStatus } from './TaskStatus'; import { TaskError } from './TaskError'; -import { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer'; +import type { ProjectChangeAnalyzer } from '../ProjectChangeAnalyzer'; import { BaseTaskRunner, ITaskRunnerContext } from './BaseTaskRunner'; import { ProjectLogWritable } from './ProjectLogWritable'; import { ProjectBuildCache } from '../buildCache/ProjectBuildCache'; -import { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; +import type { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration'; import { ICacheOptionsForCommand, RushProjectConfiguration } from '../../api/RushProjectConfiguration'; import { CollatedTerminalProvider } from '../../utilities/CollatedTerminalProvider'; -import { CommandLineConfiguration } from '../../api/CommandLineConfiguration'; +import type { CommandLineConfiguration, IPhase } from '../../api/CommandLineConfiguration'; import { RushConstants } from '../RushConstants'; +import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; export interface IProjectDeps { files: { [filePath: string]: string }; @@ -49,13 +50,11 @@ export interface IProjectTaskRunnerOptions { rushConfiguration: RushConfiguration; buildCacheConfiguration: BuildCacheConfiguration | undefined; commandToRun: string; - commandName: string; isIncrementalBuildAllowed: boolean; projectChangeAnalyzer: ProjectChangeAnalyzer; - packageDepsFilename: string; allowWarningsInSuccessfulBuild?: boolean; taskName: string; - logFilenameIdentifier: string; + phase: IPhase; } function _areShallowEqual(object1: JsonObject, object2: JsonObject): boolean { @@ -79,13 +78,12 @@ function _areShallowEqual(object1: JsonObject, object2: JsonObject): boolean { export class ProjectTaskRunner extends BaseTaskRunner { public readonly name: string; - /** - * This property is mutated by TaskExecutionManager, so is not readonly - */ - public isSkipAllowed: boolean; + public readonly isSkipAllowed: boolean; public hadEmptyScript: boolean = false; + public readonly warningsAreAllowed: boolean; private readonly _rushProject: RushConfigurationProject; + private readonly _phase: IPhase; private readonly _rushConfiguration: RushConfiguration; private readonly _buildCacheConfiguration: BuildCacheConfiguration | undefined; private readonly _commandName: string; @@ -93,7 +91,6 @@ export class ProjectTaskRunner extends BaseTaskRunner { private readonly _isCacheReadAllowed: boolean; private readonly _projectChangeAnalyzer: ProjectChangeAnalyzer; private readonly _packageDepsFilename: string; - private readonly _allowWarningsInSuccessfulBuild: boolean; private readonly _logFilenameIdentifier: string; /** @@ -104,18 +101,23 @@ export class ProjectTaskRunner extends BaseTaskRunner { public constructor(options: IProjectTaskRunnerOptions) { super(); + const phase: IPhase = options.phase; this.name = options.taskName; this._rushProject = options.rushProject; + this._phase = phase; this._rushConfiguration = options.rushConfiguration; this._buildCacheConfiguration = options.buildCacheConfiguration; - this._commandName = options.commandName; + this._commandName = phase.name; this._commandToRun = options.commandToRun; this._isCacheReadAllowed = options.isIncrementalBuildAllowed; this.isSkipAllowed = options.isIncrementalBuildAllowed; this._projectChangeAnalyzer = options.projectChangeAnalyzer; - this._packageDepsFilename = options.packageDepsFilename; - this._allowWarningsInSuccessfulBuild = options.allowWarningsInSuccessfulBuild || false; - this._logFilenameIdentifier = options.logFilenameIdentifier; + this._packageDepsFilename = `package-deps_${phase.logFilenameIdentifier}.json`; + this.warningsAreAllowed = + EnvironmentConfiguration.allowWarningsInSuccessfulBuild || + options.allowWarningsInSuccessfulBuild || + false; + this._logFilenameIdentifier = phase.logFilenameIdentifier; } public async executeAsync(context: ITaskRunnerContext): Promise { @@ -129,19 +131,6 @@ export class ProjectTaskRunner extends BaseTaskRunner { } } - public async tryWriteCacheEntryAsync( - terminal: ITerminal, - trackedFilePaths: string[] | undefined, - repoCommandLineConfiguration: CommandLineConfiguration | undefined - ): Promise { - const projectBuildCache: ProjectBuildCache | undefined = await this._getProjectBuildCacheAsync( - terminal, - trackedFilePaths, - repoCommandLineConfiguration - ); - return projectBuildCache?.trySetCacheEntryAsync(terminal); - } - private async _executeTaskAsync(context: ITaskRunnerContext): Promise { // TERMINAL PIPELINE: // @@ -270,7 +259,7 @@ export class ProjectTaskRunner extends BaseTaskRunner { // let buildCacheReadAttempted: boolean = false; if (this._isCacheReadAllowed) { - const projectBuildCache: ProjectBuildCache | undefined = await this._getProjectBuildCacheAsync( + const projectBuildCache: ProjectBuildCache | undefined = await this._tryGetProjectBuildCacheAsync( terminal, trackedFiles, context.repoCommandLineConfiguration @@ -365,7 +354,7 @@ export class ProjectTaskRunner extends BaseTaskRunner { const taskIsSuccessful: boolean = status === TaskStatus.Success || (status === TaskStatus.SuccessWithWarning && - this._allowWarningsInSuccessfulBuild && + this.warningsAreAllowed && !!this._rushConfiguration.experimentsConfiguration.configuration .buildCacheWithAllowWarningsInSuccessfulBuild); @@ -377,11 +366,13 @@ export class ProjectTaskRunner extends BaseTaskRunner { // If the command is successful and we can calculate project hash, we will write a // new cache entry even if incremental execution is not allowed. - const setCacheEntryPromise: Promise = this.tryWriteCacheEntryAsync( + const projectBuildCache: ProjectBuildCache | undefined = await this._tryGetProjectBuildCacheAsync( terminal, trackedFiles, context.repoCommandLineConfiguration ); + const setCacheEntryPromise: Promise | undefined = + projectBuildCache?.trySetCacheEntryAsync(terminal); const [, cacheWriteSuccess] = await Promise.all([writeProjectStatePromise, setCacheEntryPromise]); @@ -406,10 +397,10 @@ export class ProjectTaskRunner extends BaseTaskRunner { } } - private async _getProjectBuildCacheAsync( + private async _tryGetProjectBuildCacheAsync( terminal: ITerminal, trackedProjectFiles: string[] | undefined, - commandLineConfiguration: CommandLineConfiguration | undefined + commandLineConfiguration: CommandLineConfiguration ): Promise { if (this._projectBuildCache === UNINITIALIZED) { this._projectBuildCache = undefined; @@ -432,13 +423,17 @@ export class ProjectTaskRunner extends BaseTaskRunner { `Caching has been disabled for this project's "${this._commandName}" command.` ); } else { + const projectOutputFolderNames: ReadonlyArray = + projectConfiguration.projectOutputFolderNamesForPhases.get(this._phase.name) || []; this._projectBuildCache = await ProjectBuildCache.tryGetProjectBuildCache({ projectConfiguration, + projectOutputFolderNames, buildCacheConfiguration: this._buildCacheConfiguration, terminal, command: this._commandToRun, trackedProjectFiles: trackedProjectFiles, - projectChangeAnalyzer: this._projectChangeAnalyzer + projectChangeAnalyzer: this._projectChangeAnalyzer, + phaseName: this._phase.name }); } } diff --git a/apps/rush-lib/src/logic/taskExecution/TaskExecutionManager.ts b/apps/rush-lib/src/logic/taskExecution/TaskExecutionManager.ts index a52112c2d3f..0c7cf39977d 100644 --- a/apps/rush-lib/src/logic/taskExecution/TaskExecutionManager.ts +++ b/apps/rush-lib/src/logic/taskExecution/TaskExecutionManager.ts @@ -25,8 +25,7 @@ export interface ITaskExecutionManagerOptions { debugMode: boolean; parallelism: string | undefined; changedProjectsOnly: boolean; - allowWarningsInSuccessfulExecution: boolean; - repoCommandLineConfiguration: CommandLineConfiguration | undefined; + repoCommandLineConfiguration: CommandLineConfiguration; destination?: TerminalWritable; } @@ -44,14 +43,13 @@ const ASCII_HEADER_WIDTH: number = 79; export class TaskExecutionManager { private readonly _tasks: Task[]; private readonly _changedProjectsOnly: boolean; - private readonly _allowWarningsInSuccessfulExecution: boolean; private readonly _taskQueue: Task[]; private readonly _quietMode: boolean; private readonly _debugMode: boolean; private readonly _parallelism: number; - private readonly _repoCommandLineConfiguration: CommandLineConfiguration | undefined; + private readonly _repoCommandLineConfiguration: CommandLineConfiguration; private _hasAnyFailures: boolean; - private _hasAnyWarnings: boolean; + private _hasAnyNonAllowedWarnings: boolean; private _currentActiveTasks!: number; private _totalTasks!: number; private _completedTasks!: number; @@ -63,22 +61,14 @@ export class TaskExecutionManager { private _terminal: CollatedTerminal; public constructor(orderedTasks: Task[], options: ITaskExecutionManagerOptions) { - const { - quietMode, - debugMode, - parallelism, - changedProjectsOnly, - allowWarningsInSuccessfulExecution, - repoCommandLineConfiguration - } = options; + const { quietMode, debugMode, parallelism, changedProjectsOnly, repoCommandLineConfiguration } = options; this._tasks = orderedTasks; this._taskQueue = [...orderedTasks]; // Clone the task array this._quietMode = quietMode; this._debugMode = debugMode; this._hasAnyFailures = false; - this._hasAnyWarnings = false; + this._hasAnyNonAllowedWarnings = false; this._changedProjectsOnly = changedProjectsOnly; - this._allowWarningsInSuccessfulExecution = allowWarningsInSuccessfulExecution; this._repoCommandLineConfiguration = repoCommandLineConfiguration; // TERMINAL PIPELINE: @@ -191,7 +181,7 @@ export class TaskExecutionManager { if (this._hasAnyFailures) { this._terminal.writeStderrLine(colors.red('Projects failed to build.') + '\n'); throw new AlreadyReportedError(); - } else if (this._hasAnyWarnings && !this._allowWarningsInSuccessfulExecution) { + } else if (this._hasAnyNonAllowedWarnings) { this._terminal.writeStderrLine(colors.yellow('Projects succeeded with warnings.') + '\n'); throw new AlreadyReportedError(); } @@ -262,7 +252,7 @@ export class TaskExecutionManager { this._markTaskAsSuccess(task); break; case TaskStatus.SuccessWithWarning: - this._hasAnyWarnings = true; + this._hasAnyNonAllowedWarnings = this._hasAnyNonAllowedWarnings || !task.runner.warningsAreAllowed; this._markTaskAsSuccessWithWarning(task); break; case TaskStatus.FromCache: diff --git a/apps/rush-lib/src/logic/taskExecution/test/MockTaskRunner.ts b/apps/rush-lib/src/logic/taskExecution/test/MockTaskRunner.ts index 0d6348166e4..b5cd1678b4f 100644 --- a/apps/rush-lib/src/logic/taskExecution/test/MockTaskRunner.ts +++ b/apps/rush-lib/src/logic/taskExecution/test/MockTaskRunner.ts @@ -11,12 +11,18 @@ export class MockTaskRunner extends BaseTaskRunner { public readonly name: string; public readonly hadEmptyScript: boolean = false; public readonly isSkipAllowed: boolean = false; + public readonly warningsAreAllowed: boolean; - public constructor(name: string, action?: (terminal: CollatedTerminal) => Promise) { + public constructor( + name: string, + action?: (terminal: CollatedTerminal) => Promise, + warningsAreAllowed: boolean = false + ) { super(); this.name = name; this._action = action; + this.warningsAreAllowed = warningsAreAllowed; } public async executeAsync(context: ITaskRunnerContext): Promise { diff --git a/apps/rush-lib/src/logic/taskExecution/test/TaskExecutionManager.test.ts b/apps/rush-lib/src/logic/taskExecution/test/TaskExecutionManager.test.ts index f444f062c08..f4d26734898 100644 --- a/apps/rush-lib/src/logic/taskExecution/test/TaskExecutionManager.test.ts +++ b/apps/rush-lib/src/logic/taskExecution/test/TaskExecutionManager.test.ts @@ -70,8 +70,7 @@ describe(TaskExecutionManager.name, () => { parallelism: 'tequila', changedProjectsOnly: false, destination: mockWritable, - allowWarningsInSuccessfulExecution: false, - repoCommandLineConfiguration: undefined + repoCommandLineConfiguration: undefined! }) ).toThrowErrorMatchingSnapshot(); }); @@ -85,8 +84,7 @@ describe(TaskExecutionManager.name, () => { parallelism: '1', changedProjectsOnly: false, destination: mockWritable, - allowWarningsInSuccessfulExecution: false, - repoCommandLineConfiguration: undefined + repoCommandLineConfiguration: undefined! }; }); @@ -143,8 +141,7 @@ describe(TaskExecutionManager.name, () => { parallelism: '1', changedProjectsOnly: false, destination: mockWritable, - allowWarningsInSuccessfulExecution: false, - repoCommandLineConfiguration: undefined + repoCommandLineConfiguration: undefined! }; }); @@ -179,19 +176,22 @@ describe(TaskExecutionManager.name, () => { parallelism: '1', changedProjectsOnly: false, destination: mockWritable, - allowWarningsInSuccessfulExecution: true, - repoCommandLineConfiguration: undefined + repoCommandLineConfiguration: undefined! }; }); it('Logs warnings correctly', async () => { taskExecutionManager = createTaskExecutionManager( taskExecutionManagerOptions, - new MockTaskRunner('success with warnings (success)', async (terminal: CollatedTerminal) => { - terminal.writeStdoutLine('Build step 1' + EOL); - terminal.writeStdoutLine('Warning: step 1 succeeded with warnings' + EOL); - return TaskStatus.SuccessWithWarning; - }) + new MockTaskRunner( + 'success with warnings (success)', + async (terminal: CollatedTerminal) => { + terminal.writeStdoutLine('Build step 1' + EOL); + terminal.writeStdoutLine('Warning: step 1 succeeded with warnings' + EOL); + return TaskStatus.SuccessWithWarning; + }, + /* warningsAreAllowed */ true + ) ); await taskExecutionManager.executeAsync(); diff --git a/apps/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts b/apps/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts index f22cd454699..47a5818cd52 100644 --- a/apps/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts +++ b/apps/rush-lib/src/logic/test/ProjectChangeAnalyzer.test.ts @@ -9,11 +9,12 @@ import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration'; import { RushConfigurationProject } from '../../api/RushConfigurationProject'; import { RushProjectConfiguration } from '../../api/RushProjectConfiguration'; import { LookupByPath } from '../LookupByPath'; +import { UNINITIALIZED } from '../../utilities/Utilities'; describe(ProjectChangeAnalyzer.name, () => { beforeEach(() => { jest.spyOn(EnvironmentConfiguration, 'gitBinaryPath', 'get').mockReturnValue(undefined); - jest.spyOn(RushProjectConfiguration, 'tryLoadForProjectAsync').mockResolvedValue(undefined); + jest.spyOn(RushProjectConfiguration, 'tryLoadIgnoreGlobsForProjectAsync').mockResolvedValue(undefined); }); afterEach(() => { @@ -87,11 +88,13 @@ describe(ProjectChangeAnalyzer.name, () => { it('ignores files specified by project configuration files, relative to project folder', async () => { // rush-project.json configuration for 'apple' - jest.spyOn(RushProjectConfiguration, 'tryLoadForProjectAsync').mockResolvedValueOnce({ - incrementalBuildIgnoredGlobs: ['assets/*.png', '*.js.map'] as ReadonlyArray - } as RushProjectConfiguration); + jest + .spyOn(RushProjectConfiguration, 'tryLoadIgnoreGlobsForProjectAsync') + .mockResolvedValueOnce(['assets/*.png', '*.js.map']); // rush-project.json configuration for 'banana' does not exist - jest.spyOn(RushProjectConfiguration, 'tryLoadForProjectAsync').mockResolvedValueOnce(undefined); + jest + .spyOn(RushProjectConfiguration, 'tryLoadIgnoreGlobsForProjectAsync') + .mockResolvedValueOnce(undefined); const projects: RushConfigurationProject[] = [ { @@ -132,13 +135,9 @@ describe(ProjectChangeAnalyzer.name, () => { it('interprets ignored globs as a dot-ignore file (not as individually handled globs)', async () => { // rush-project.json configuration for 'apple' - jest.spyOn(RushProjectConfiguration, 'tryLoadForProjectAsync').mockResolvedValue({ - incrementalBuildIgnoredGlobs: [ - '*.png', - 'assets/*.psd', - '!assets/important/**' - ] as ReadonlyArray - } as RushProjectConfiguration); + jest + .spyOn(RushProjectConfiguration, 'tryLoadIgnoreGlobsForProjectAsync') + .mockResolvedValue(['*.png', 'assets/*.psd', '!assets/important/**']); const projects: RushConfigurationProject[] = [ { @@ -247,11 +246,12 @@ describe(ProjectChangeAnalyzer.name, () => { // ProjectChangeAnalyzer is inert until someone actually requests project data, // this test makes that expectation explicit. - expect(subject['_data']).toEqual(undefined); + expect(subject['_data']).toEqual(UNINITIALIZED); expect(await subject._tryGetProjectDependenciesAsync(projects[0], terminal)).toEqual( new Map([['apps/apple/core.js', 'a101']]) ); expect(subject['_data']).toBeDefined(); + expect(subject['_data']).not.toEqual(UNINITIALIZED); expect(await subject._tryGetProjectDependenciesAsync(projects[0], terminal)).toEqual( new Map([['apps/apple/core.js', 'a101']]) ); diff --git a/apps/rush-lib/src/schemas/experiments.schema.json b/apps/rush-lib/src/schemas/experiments.schema.json index 96feb9d7c32..8b8f776e00e 100644 --- a/apps/rush-lib/src/schemas/experiments.schema.json +++ b/apps/rush-lib/src/schemas/experiments.schema.json @@ -30,8 +30,8 @@ "description": "If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings. This will not replay warnings from the cached build.", "type": "boolean" }, - "_multiPhaseCommands": { - "description": "(NOT YET FEATURE COMPLETE) If true, the multi-phase commands feature is enabled. To use this feature, create a \"phased\" command in common/config/rush/command-line.json.", + "phasedCommands": { + "description": "If true, the phased commands feature is enabled. To use this feature, create a \"phased\" command in common/config/rush/command-line.json.", "type": "boolean" } }, diff --git a/apps/rush-lib/src/utilities/Utilities.ts b/apps/rush-lib/src/utilities/Utilities.ts index a2e5ebc29de..f91b60e570a 100644 --- a/apps/rush-lib/src/utilities/Utilities.ts +++ b/apps/rush-lib/src/utilities/Utilities.ts @@ -594,10 +594,6 @@ export class Utilities { return new Error('Unable to find rush.json configuration file'); } - public static getPackageDepsFilenameForCommand(command: string): string { - return `package-deps_${command}.json`; - } - public static async usingAsync( getDisposableAsync: () => Promise | IDisposable, doActionAsync: (disposable: TDisposable) => Promise | void diff --git a/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-47.json b/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-47.json new file mode 100644 index 00000000000..5e6c1b7be32 --- /dev/null +++ b/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-47.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "(BREAKING CHANGE) Remove the experimental command \"rush write-build-cache\", since it is no longer needed and would be incompatible with phased builds. If you need this command for some reason, please create a GitHub issue.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-52.json b/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-52.json new file mode 100644 index 00000000000..9a344497c71 --- /dev/null +++ b/common/changes/@microsoft/rush/phased-commands_2021-12-25-04-52.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for phased commands behind the multiPhaseCommands experiment.", + "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 86fc8f7184b..cebfb4b4029 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -241,9 +241,9 @@ export interface IEnvironmentConfigurationInitializeOptions { // @beta export interface IExperimentsJson { buildCacheWithAllowWarningsInSuccessfulBuild?: boolean; - _multiPhaseCommands?: boolean; noChmodFieldInTarHeaderNormalization?: boolean; omitImportersFromPreventManualShrinkwrapChanges?: boolean; + phasedCommands?: boolean; usePnpmFrozenLockfileForRushInstall?: boolean; usePnpmPreferFrozenLockfileForRushUpdate?: boolean; } @@ -627,6 +627,7 @@ export class RushConstants { static readonly nonbrowserApprovedPackagesFilename: string; static readonly npmShrinkwrapFilename: string; static readonly phasedCommandKind: 'phased'; + static readonly phaseNamePrefix: '_phase:'; // @deprecated static readonly pinnedVersionsFilename: string; static readonly pnpmfileV1Filename: string;