diff --git a/common/changes/@microsoft/rush/copilot-add-project-level-parameter-ignoring_2025-11-13-00-17.json b/common/changes/@microsoft/rush/copilot-add-project-level-parameter-ignoring_2025-11-13-00-17.json new file mode 100644 index 00000000000..977b38c66c7 --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-add-project-level-parameter-ignoring_2025-11-13-00-17.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add project-level parameter ignoring to prevent unnecessary cache invalidation. Projects can now use \"parameterNamesToIgnore\" in \"rush-project.json\" to exclude custom command-line parameters that don't affect their operations.", + "type": "minor", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "198982749+Copilot@users.noreply.github.com" +} \ 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 d6804daab7e..cf0b56b9abb 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -670,6 +670,7 @@ export interface IOperationSettings { ignoreChangedProjectsOnlyFlag?: boolean; operationName: string; outputFolderNames?: string[]; + parameterNamesToIgnore?: string[]; sharding?: IRushPhaseSharding; weight?: number; } diff --git a/libraries/rush-lib/src/api/RushProjectConfiguration.ts b/libraries/rush-lib/src/api/RushProjectConfiguration.ts index 75b7216f47b..6ee20c8e91a 100644 --- a/libraries/rush-lib/src/api/RushProjectConfiguration.ts +++ b/libraries/rush-lib/src/api/RushProjectConfiguration.ts @@ -146,6 +146,14 @@ export interface IOperationSettings { * If true, this operation will never be skipped by the `--changed-projects-only` flag. */ ignoreChangedProjectsOnlyFlag?: boolean; + + /** + * An optional list of custom command-line parameter names (their `parameterLongName` values from + * command-line.json) that should be ignored when invoking the command for this operation. + * This allows a project to opt out of parameters that don't affect its operation, preventing + * unnecessary cache invalidation for this operation and its consumers. + */ + parameterNamesToIgnore?: string[]; } interface IOldRushProjectJson { diff --git a/libraries/rush-lib/src/cli/parsing/associateParametersByPhase.ts b/libraries/rush-lib/src/cli/parsing/associateParametersByPhase.ts new file mode 100644 index 00000000000..408b1f27910 --- /dev/null +++ b/libraries/rush-lib/src/cli/parsing/associateParametersByPhase.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { InternalError } from '@rushstack/node-core-library'; +import type { CommandLineParameter } from '@rushstack/ts-command-line'; + +import type { IParameterJson, IPhase } from '../../api/CommandLineConfiguration'; + +/** + * Associates command line parameters with their associated phases. + * This helper is used to populate the `associatedParameters` set on each phase + * based on the `associatedPhases` property of each parameter. + * + * @param customParameters - Map of parameter definitions to their CommandLineParameter instances + * @param knownPhases - Map of phase names to IPhase objects + */ +export function associateParametersByPhase( + customParameters: ReadonlyMap, + knownPhases: ReadonlyMap +): void { + for (const [parameterJson, tsCommandLineParameter] of customParameters) { + if (parameterJson.associatedPhases) { + for (const phaseName of parameterJson.associatedPhases) { + const phase: IPhase | undefined = knownPhases.get(phaseName); + if (!phase) { + throw new InternalError(`Could not find a phase matching ${phaseName}.`); + } + phase.associatedParameters.add(tsCommandLineParameter); + } + } + } +} diff --git a/libraries/rush-lib/src/cli/parsing/defineCustomParameters.ts b/libraries/rush-lib/src/cli/parsing/defineCustomParameters.ts new file mode 100644 index 00000000000..bd5b80758dc --- /dev/null +++ b/libraries/rush-lib/src/cli/parsing/defineCustomParameters.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { CommandLineAction, CommandLineParameter } from '@rushstack/ts-command-line'; + +import type { IParameterJson } from '../../api/CommandLineConfiguration'; +import { RushConstants } from '../../logic/RushConstants'; +import type { ParameterJson } from '../../api/CommandLineJson'; + +/** + * Helper function to create CommandLineParameter instances from parameter definitions. + * This centralizes the logic for defining parameters based on their kind. + * + * @param action - The CommandLineAction to define the parameters on + * @param associatedParameters - The set of parameter definitions + * @param targetMap - The map to populate with parameter definitions to CommandLineParameter instances + */ +export function defineCustomParameters( + action: CommandLineAction, + associatedParameters: Iterable, + targetMap: Map +): void { + for (const parameter of associatedParameters) { + let tsCommandLineParameter: CommandLineParameter | undefined; + + switch (parameter.parameterKind) { + case 'flag': + tsCommandLineParameter = action.defineFlagParameter({ + parameterShortName: parameter.shortName, + parameterLongName: parameter.longName, + description: parameter.description, + required: parameter.required + }); + break; + case 'choice': + tsCommandLineParameter = action.defineChoiceParameter({ + 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': + tsCommandLineParameter = action.defineStringParameter({ + parameterLongName: parameter.longName, + parameterShortName: parameter.shortName, + description: parameter.description, + required: parameter.required, + argumentName: parameter.argumentName + }); + break; + case 'integer': + tsCommandLineParameter = action.defineIntegerParameter({ + parameterLongName: parameter.longName, + parameterShortName: parameter.shortName, + description: parameter.description, + required: parameter.required, + argumentName: parameter.argumentName + }); + break; + case 'stringList': + tsCommandLineParameter = action.defineStringListParameter({ + parameterLongName: parameter.longName, + parameterShortName: parameter.shortName, + description: parameter.description, + required: parameter.required, + argumentName: parameter.argumentName + }); + break; + case 'integerList': + tsCommandLineParameter = action.defineIntegerListParameter({ + parameterLongName: parameter.longName, + parameterShortName: parameter.shortName, + description: parameter.description, + required: parameter.required, + argumentName: parameter.argumentName + }); + break; + case 'choiceList': + tsCommandLineParameter = action.defineChoiceListParameter({ + parameterShortName: parameter.shortName, + parameterLongName: parameter.longName, + description: parameter.description, + required: parameter.required, + alternatives: parameter.alternatives.map((x) => x.name) + }); + break; + default: + throw new Error( + `${RushConstants.commandLineFilename} defines a parameter "${ + (parameter as ParameterJson).longName + }" using an unsupported parameter kind "${(parameter as ParameterJson).parameterKind}"` + ); + } + + targetMap.set(parameter, tsCommandLineParameter); + } +} diff --git a/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts index 2da245a5b7a..3d22215e517 100644 --- a/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/BaseScriptAction.ts @@ -5,8 +5,7 @@ import type { CommandLineParameter } from '@rushstack/ts-command-line'; import { BaseRushAction, type IBaseRushActionOptions } from '../actions/BaseRushAction'; import type { Command, CommandLineConfiguration, IParameterJson } from '../../api/CommandLineConfiguration'; -import { RushConstants } from '../../logic/RushConstants'; -import type { ParameterJson } from '../../api/CommandLineJson'; +import { defineCustomParameters } from '../parsing/defineCustomParameters'; /** * Constructor parameters for BaseScriptAction @@ -42,83 +41,7 @@ export abstract class BaseScriptAction extends BaseRus return; } - // Find any parameters that are associated with this command - for (const parameter of this.command.associatedParameters) { - let tsCommandLineParameter: CommandLineParameter | undefined; - - switch (parameter.parameterKind) { - case 'flag': - tsCommandLineParameter = this.defineFlagParameter({ - parameterShortName: parameter.shortName, - parameterLongName: parameter.longName, - description: parameter.description, - required: parameter.required - }); - break; - case 'choice': - tsCommandLineParameter = this.defineChoiceParameter({ - 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': - tsCommandLineParameter = this.defineStringParameter({ - parameterLongName: parameter.longName, - parameterShortName: parameter.shortName, - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName - }); - break; - case 'integer': - tsCommandLineParameter = this.defineIntegerParameter({ - parameterLongName: parameter.longName, - parameterShortName: parameter.shortName, - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName - }); - break; - case 'stringList': - tsCommandLineParameter = this.defineStringListParameter({ - parameterLongName: parameter.longName, - parameterShortName: parameter.shortName, - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName - }); - break; - case 'integerList': - tsCommandLineParameter = this.defineIntegerListParameter({ - parameterLongName: parameter.longName, - parameterShortName: parameter.shortName, - description: parameter.description, - required: parameter.required, - argumentName: parameter.argumentName - }); - break; - case 'choiceList': - tsCommandLineParameter = this.defineChoiceListParameter({ - parameterShortName: parameter.shortName, - parameterLongName: parameter.longName, - description: parameter.description, - required: parameter.required, - alternatives: parameter.alternatives.map((x) => x.name) - }); - break; - default: - throw new Error( - `${RushConstants.commandLineFilename} defines a parameter "${ - (parameter as ParameterJson).longName - }" using an unsupported parameter kind "${(parameter as ParameterJson).parameterKind}"` - ); - } - - this.customParameters.set(parameter, tsCommandLineParameter); - } + // Use the centralized helper to create CommandLineParameter instances + defineCustomParameters(this, this.command.associatedParameters, this.customParameters); } } diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 906a3ca89ea..c5f19629af5 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -3,7 +3,7 @@ import type { AsyncSeriesHook } from 'tapable'; -import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library'; +import { AlreadyReportedError } from '@rushstack/node-core-library'; import { type ITerminal, Terminal, Colorize } from '@rushstack/terminal'; import type { CommandLineFlagParameter, @@ -33,6 +33,7 @@ import { SelectionParameterSet } from '../parsing/SelectionParameterSet'; import type { IPhase, IPhasedCommandConfig } from '../../api/CommandLineConfiguration'; import type { Operation } from '../../logic/operations/Operation'; import type { OperationExecutionRecord } from '../../logic/operations/OperationExecutionRecord'; +import { associateParametersByPhase } from '../parsing/associateParametersByPhase'; import { PhasedOperationPlugin } from '../../logic/operations/PhasedOperationPlugin'; import { ShellOperationRunnerPlugin } from '../../logic/operations/ShellOperationRunnerPlugin'; import { Event } from '../../api/EventHooks'; @@ -327,17 +328,8 @@ export class PhasedScriptAction extends BaseScriptAction i this.defineScriptParameters(); - for (const [{ associatedPhases }, tsCommandLineParameter] of this.customParameters) { - if (associatedPhases) { - for (const phaseName of associatedPhases) { - const phase: IPhase | undefined = this._knownPhases.get(phaseName); - if (!phase) { - throw new InternalError(`Could not find a phase matching ${phaseName}.`); - } - phase.associatedParameters.add(tsCommandLineParameter); - } - } - } + // Associate parameters with their respective phases + associateParametersByPhase(this.customParameters, this._knownPhases); } public async runAsync(): Promise { diff --git a/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts b/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts index b72151fdb11..6fbc924d87e 100644 --- a/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/IPCOperationRunner.ts @@ -30,6 +30,7 @@ export interface IIPCOperationRunnerOptions { commandForHash: string; persist: boolean; requestRun: OperationRequestRunCallback; + ignoredParameterValues: ReadonlyArray; } function isAfterExecuteEventMessage(message: unknown): message is IAfterExecuteEventMessage { @@ -59,6 +60,7 @@ export class IPCOperationRunner implements IOperationRunner { private readonly _commandForHash: string; private readonly _persist: boolean; private readonly _requestRun: OperationRequestRunCallback; + private readonly _ignoredParameterValues: ReadonlyArray; private _ipcProcess: ChildProcess | undefined; private _processReadyPromise: Promise | undefined; @@ -75,6 +77,7 @@ export class IPCOperationRunner implements IOperationRunner { this._persist = options.persist; this._requestRun = options.requestRun; + this._ignoredParameterValues = options.ignoredParameterValues; } public async executeAsync(context: IOperationRunnerContext): Promise { @@ -82,6 +85,13 @@ export class IPCOperationRunner implements IOperationRunner { async (terminal: ITerminal, terminalProvider: ITerminalProvider): Promise => { let isConnected: boolean = false; if (!this._ipcProcess || typeof this._ipcProcess.exitCode === 'number') { + // Log any ignored parameters + if (this._ignoredParameterValues.length > 0) { + terminal.writeLine( + `These parameters were ignored for this operation by project-level configuration: ${this._ignoredParameterValues.join(' ')}` + ); + } + // Run the operation terminal.writeLine('Invoking: ' + this._commandToRun); diff --git a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts index 550cc726889..f87dcb1685a 100644 --- a/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/IPCOperationRunnerPlugin.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { IPhase } from '../../api/CommandLineConfiguration'; import type { ICreateOperationsContext, IPhasedCommandPlugin, @@ -14,7 +13,8 @@ import { OperationStatus } from './OperationStatus'; import { PLUGIN_NAME as ShellOperationPluginName, formatCommand, - getCustomParameterValuesByPhase, + getCustomParameterValuesByOperation, + type ICustomParameterValuesForOperation, getDisplayName } from './ShellOperationRunnerPlugin'; @@ -45,8 +45,8 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { currentContext = context; - const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + const getCustomParameterValues: (operation: Operation) => ICustomParameterValuesForOperation = + getCustomParameterValuesByOperation(); for (const operation of operations) { const { associatedPhase: phase, associatedProject: project, runner } = operation; @@ -73,7 +73,8 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { // for this operation (or downstream operations) to be restored from the build cache. const commandForHash: string | undefined = phase.shellCommand ?? scripts?.[phaseName]; - const customParameterValues: ReadonlyArray = getCustomParameterValuesForPhase(phase); + const { parameterValues: customParameterValues, ignoredParameterValues } = + getCustomParameterValues(operation); const commandToRun: string = formatCommand(rawScript, customParameterValues); const operationName: string = getDisplayName(phase, project); @@ -86,6 +87,7 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin { commandToRun, commandForHash, persist: true, + ignoredParameterValues, requestRun: (requestor: string, detail?: string) => { const operationState: IOperationExecutionResult | undefined = operationStatesByRunner.get(ipcOperationRunner); diff --git a/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts b/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts index 20a0ce44dbd..b4f017dfe3f 100644 --- a/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import type { IPhase } from '../../api/CommandLineConfiguration'; import type { IOperationSettings, RushProjectConfiguration } from '../../api/RushProjectConfiguration'; import type { ICreateOperationsContext, @@ -13,7 +12,8 @@ import { NullOperationRunner } from './NullOperationRunner'; import { Operation } from './Operation'; import { OperationStatus } from './OperationStatus'; import { - getCustomParameterValuesByPhase, + getCustomParameterValuesByOperation, + type ICustomParameterValuesForOperation, getDisplayName, initializeShellOperationRunner } from './ShellOperationRunnerPlugin'; @@ -46,8 +46,8 @@ export class ShardedPhasedOperationPlugin implements IPhasedCommandPlugin { function spliceShards(existingOperations: Set, context: ICreateOperationsContext): Set { const { rushConfiguration, projectConfigurations } = context; - const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + const getCustomParameterValues: (operation: Operation) => ICustomParameterValuesForOperation = + getCustomParameterValuesByOperation(); for (const operation of existingOperations) { const { @@ -119,10 +119,12 @@ function spliceShards(existingOperations: Set, context: ICreateOperat const collatorDisplayName: string = `${getDisplayName(phase, project)} - collate`; - const customParameters: readonly string[] = getCustomParameterValuesForPhase(phase); + // Get the custom parameter values for the collator, filtered according to the operation settings + const { parameterValues: customParameterValues, ignoredParameterValues } = + getCustomParameterValues(operation); const collatorParameters: string[] = [ - ...customParameters, + ...customParameterValues, `--shard-parent-folder="${parentFolder}"`, `--shard-count="${shards}"` ]; @@ -137,7 +139,8 @@ function spliceShards(existingOperations: Set, context: ICreateOperat displayName: collatorDisplayName, rushConfiguration, commandToRun, - customParameterValues: collatorParameters + customParameterValues: collatorParameters, + ignoredParameterValues }); const shardOperationName: string = `${phase.name}:shard`; @@ -194,7 +197,7 @@ function spliceShards(existingOperations: Set, context: ICreateOperat ); const shardedParameters: string[] = [ - ...customParameters, + ...customParameterValues, shardArgument, outputDirectoryArgumentWithShard ]; @@ -207,7 +210,8 @@ function spliceShards(existingOperations: Set, context: ICreateOperat commandToRun: baseCommand, customParameterValues: shardedParameters, displayName: shardDisplayName, - rushConfiguration + rushConfiguration, + ignoredParameterValues }); shardOperation.addDependency(preShardOperation); diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts index 5e3200c1bdf..3f9f5dc6ddb 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunner.ts @@ -20,6 +20,7 @@ export interface IShellOperationRunnerOptions { displayName: string; commandToRun: string; commandForHash: string; + ignoredParameterValues: ReadonlyArray; } /** @@ -44,6 +45,8 @@ export class ShellOperationRunner implements IOperationRunner { private readonly _rushProject: RushConfigurationProject; + private readonly _ignoredParameterValues: ReadonlyArray; + public constructor(options: IShellOperationRunnerOptions) { const { phase } = options; @@ -53,6 +56,7 @@ export class ShellOperationRunner implements IOperationRunner { this._rushProject = options.rushProject; this.commandToRun = options.commandToRun; this._commandForHash = options.commandForHash; + this._ignoredParameterValues = options.ignoredParameterValues; } public async executeAsync(context: IOperationRunnerContext): Promise { @@ -72,6 +76,13 @@ export class ShellOperationRunner implements IOperationRunner { async (terminal: ITerminal, terminalProvider: ITerminalProvider) => { let hasWarningOrError: boolean = false; + // Log any ignored parameters + if (this._ignoredParameterValues.length > 0) { + terminal.writeLine( + `These parameters were ignored for this operation by project-level configuration: ${this._ignoredParameterValues.join(' ')}` + ); + } + // Run the operation terminal.writeLine(`Invoking: ${this.commandToRun}`); diff --git a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts index 96186d9e0d8..e48a52482f6 100644 --- a/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/ShellOperationRunnerPlugin.ts @@ -31,15 +31,17 @@ export class ShellOperationRunnerPlugin implements IPhasedCommandPlugin { ): Set { const { rushConfiguration, isInitial } = context; - const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray = - getCustomParameterValuesByPhase(); + const getCustomParameterValues: (operation: Operation) => ICustomParameterValuesForOperation = + getCustomParameterValuesByOperation(); + for (const operation of operations) { const { associatedPhase: phase, associatedProject: project } = operation; if (!operation.runner) { // This is a shell command. In the future, may consider having a property on the initial operation // to specify a runner type requested in rush-project.json - const customParameterValues: ReadonlyArray = getCustomParameterValuesForPhase(phase); + const { parameterValues: customParameterValues, ignoredParameterValues } = + getCustomParameterValues(operation); const displayName: string = getDisplayName(phase, project); const { name: phaseName, shellCommand } = phase; @@ -63,6 +65,7 @@ export class ShellOperationRunnerPlugin implements IPhasedCommandPlugin { commandForHash, commandToRun, customParameterValues, + ignoredParameterValues, rushConfiguration }); } @@ -82,8 +85,9 @@ export function initializeShellOperationRunner(options: { commandToRun: string | undefined; commandForHash?: string; customParameterValues: ReadonlyArray; + ignoredParameterValues: ReadonlyArray; }): IOperationRunner { - const { phase, project, commandToRun: rawCommandToRun, displayName } = options; + const { phase, project, commandToRun: rawCommandToRun, displayName, ignoredParameterValues } = options; if (typeof rawCommandToRun !== 'string' && phase.missingScriptBehavior === 'error') { throw new Error( @@ -104,7 +108,8 @@ export function initializeShellOperationRunner(options: { commandForHash, displayName, phase, - rushProject: project + rushProject: project, + ignoredParameterValues }); } else { // Empty build script indicates a no-op, so use a no-op runner @@ -116,6 +121,31 @@ export function initializeShellOperationRunner(options: { } } +/** + * Result of filtering custom parameters for an operation + */ +export interface ICustomParameterValuesForOperation { + /** + * The serialized custom parameter values that should be included in the command + */ + parameterValues: ReadonlyArray; + /** + * The serialized custom parameter values that were ignored for this operation + */ + ignoredParameterValues: ReadonlyArray; +} + +/** + * Helper function to collect all parameter arguments for a phase + */ +function collectPhaseParameterArguments(phase: IPhase): string[] { + const customParameterList: string[] = []; + for (const tsCommandLineParameter of phase.associatedParameters) { + tsCommandLineParameter.appendToArgList(customParameterList); + } + return customParameterList; +} + /** * Memoizer for custom parameter values by phase * @returns A function that returns the custom parameter values for a given phase @@ -124,20 +154,69 @@ export function getCustomParameterValuesByPhase(): (phase: IPhase) => ReadonlyAr const customParametersByPhase: Map = new Map(); function getCustomParameterValuesForPhase(phase: IPhase): ReadonlyArray { - let customParameterValues: string[] | undefined = customParametersByPhase.get(phase); - if (!customParameterValues) { - customParameterValues = []; - for (const tsCommandLineParameter of phase.associatedParameters) { - tsCommandLineParameter.appendToArgList(customParameterValues); + let customParameterList: string[] | undefined = customParametersByPhase.get(phase); + if (!customParameterList) { + customParameterList = collectPhaseParameterArguments(phase); + customParametersByPhase.set(phase, customParameterList); + } + + return customParameterList; + } + + return getCustomParameterValuesForPhase; +} + +/** + * Gets custom parameter values for an operation, filtering out any parameters that should be ignored + * based on the operation's settings. + * @returns A function that returns the filtered custom parameter values and ignored parameter values for a given operation + */ +export function getCustomParameterValuesByOperation(): ( + operation: Operation +) => ICustomParameterValuesForOperation { + const customParametersByPhase: Map = new Map(); + + function getCustomParameterValuesForOp(operation: Operation): ICustomParameterValuesForOperation { + const { associatedPhase: phase, settings } = operation; + + // Check if there are any parameters to ignore + const parameterNamesToIgnore: string[] | undefined = settings?.parameterNamesToIgnore; + if (!parameterNamesToIgnore || parameterNamesToIgnore.length === 0) { + // No filtering needed - use the cached parameter list for efficiency + let customParameterList: string[] | undefined = customParametersByPhase.get(phase); + if (!customParameterList) { + customParameterList = collectPhaseParameterArguments(phase); + customParametersByPhase.set(phase, customParameterList); } - customParametersByPhase.set(phase, customParameterValues); + return { + parameterValues: customParameterList, + ignoredParameterValues: [] + }; + } + + // Filtering is needed - we must iterate through parameter objects to check longName + // Note: We cannot use the cached parameter list here because we need access to + // the parameter objects to get their longName property for filtering + const ignoreSet: Set = new Set(parameterNamesToIgnore); + const filteredParameterValues: string[] = []; + const ignoredParameterValues: string[] = []; + + for (const tsCommandLineParameter of phase.associatedParameters) { + const parameterLongName: string = tsCommandLineParameter.longName; + + tsCommandLineParameter.appendToArgList( + ignoreSet.has(parameterLongName) ? ignoredParameterValues : filteredParameterValues + ); } - return customParameterValues; + return { + parameterValues: filteredParameterValues, + ignoredParameterValues + }; } - return getCustomParameterValuesForPhase; + return getCustomParameterValuesForOp; } export function formatCommand(rawCommand: string, customParameterValues: ReadonlyArray): string { diff --git a/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts b/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts index 912531e3eef..818144e85df 100644 --- a/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts +++ b/libraries/rush-lib/src/logic/operations/test/ShellOperationRunnerPlugin.test.ts @@ -3,9 +3,16 @@ import path from 'node:path'; import { JsonFile } from '@rushstack/node-core-library'; +import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal'; +import { CommandLineAction, CommandLineParser, type CommandLineParameter } from '@rushstack/ts-command-line'; import { RushConfiguration } from '../../../api/RushConfiguration'; -import { CommandLineConfiguration, type IPhasedCommandConfig } from '../../../api/CommandLineConfiguration'; +import { + CommandLineConfiguration, + type IPhasedCommandConfig, + type IParameterJson, + type IPhase +} from '../../../api/CommandLineConfiguration'; import type { Operation } from '../Operation'; import type { ICommandLineJson } from '../../../api/CommandLineJson'; import { PhasedOperationPlugin } from '../PhasedOperationPlugin'; @@ -14,6 +21,9 @@ import { type ICreateOperationsContext, PhasedCommandHooks } from '../../../pluginFramework/PhasedCommandHooks'; +import { RushProjectConfiguration } from '../../../api/RushProjectConfiguration'; +import { defineCustomParameters } from '../../../cli/parsing/defineCustomParameters'; +import { associateParametersByPhase } from '../../../cli/parsing/associateParametersByPhase'; interface ISerializedOperation { name: string; @@ -27,6 +37,27 @@ function serializeOperation(operation: Operation): ISerializedOperation { }; } +/** + * Test implementation of CommandLineAction for testing parameter handling + */ +class TestCommandLineAction extends CommandLineAction { + protected async onExecuteAsync(): Promise { + // No-op for testing + } +} + +/** + * Test implementation of CommandLineParser for testing parameter handling + */ +class TestCommandLineParser extends CommandLineParser { + public constructor() { + super({ + toolFilename: 'test-tool', + toolDescription: 'Test tool for parameter parsing' + }); + } +} + describe(ShellOperationRunnerPlugin.name, () => { it('shellCommand "echo custom shellCommand" should be set to commandToRun', async () => { const rushJsonFile: string = path.resolve(__dirname, `../../test/customShellCommandinBulkRepo/rush.json`); @@ -121,4 +152,127 @@ describe(ShellOperationRunnerPlugin.name, () => { // All projects expect(Array.from(operations, serializeOperation)).toMatchSnapshot(); }); + + it('parameters should be filtered when parameterNamesToIgnore is specified', async () => { + const rushJsonFile: string = path.resolve(__dirname, `../../test/parameterIgnoringRepo/rush.json`); + const commandLineJsonFile: string = path.resolve( + __dirname, + `../../test/parameterIgnoringRepo/common/config/rush/command-line.json` + ); + + const rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + const commandLineJson: ICommandLineJson = JsonFile.load(commandLineJsonFile); + + const commandLineConfiguration = new CommandLineConfiguration(commandLineJson); + const buildCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get( + 'build' + )! as IPhasedCommandConfig; + + // Load project configurations + const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider(); + const terminal: Terminal = new Terminal(terminalProvider); + + const projectConfigurations = await RushProjectConfiguration.tryLoadForProjectsAsync( + rushConfiguration.projects, + terminal + ); + + // Create CommandLineParser and action to parse parameter values + const parser: TestCommandLineParser = new TestCommandLineParser(); + const action: TestCommandLineAction = new TestCommandLineAction({ + actionName: 'build', + summary: 'Test build action', + documentation: 'Test' + }); + parser.addAction(action); + + // Create CommandLineParameter instances from the parameter definitions + const customParametersMap: Map = new Map(); + defineCustomParameters(action, buildCommand.associatedParameters, customParametersMap); + + // Parse parameter values using the parser + await parser.executeWithoutErrorHandlingAsync([ + 'build', + '--production', + '--verbose', + '--config', + '/path/to/config.json', + '--mode', + 'prod', + '--tags', + 'tag1', + '--tags', + 'tag2' + ]); + + // Associate parameters with phases using the helper + // Create a map of phase names to phases for the helper + const phasesMap: Map = new Map(); + for (const phase of buildCommand.phases) { + phasesMap.set(phase.name, phase); + } + associateParametersByPhase(customParametersMap, phasesMap); + + // Create customParameters map for ICreateOperationsContext (keyed by longName) + const customParametersForContext: Map = new Map(); + for (const [param, cli] of customParametersMap) { + customParametersForContext.set(param.longName, cli); + } + + const fakeCreateOperationsContext: Pick< + ICreateOperationsContext, + | 'phaseOriginal' + | 'phaseSelection' + | 'projectSelection' + | 'projectsInUnknownState' + | 'projectConfigurations' + | 'rushConfiguration' + | 'customParameters' + > = { + phaseOriginal: buildCommand.phases, + phaseSelection: buildCommand.phases, + projectSelection: new Set(rushConfiguration.projects), + projectsInUnknownState: new Set(rushConfiguration.projects), + projectConfigurations, + rushConfiguration, + customParameters: customParametersForContext + }; + + const hooks: PhasedCommandHooks = new PhasedCommandHooks(); + + // Generates the default operation graph + new PhasedOperationPlugin().apply(hooks); + // Applies the Shell Operation Runner to selected operations + new ShellOperationRunnerPlugin().apply(hooks); + + const operations: Set = await hooks.createOperations.promise( + new Set(), + fakeCreateOperationsContext as ICreateOperationsContext + ); + + // Verify that project 'a' has the --production parameter filtered out + const operationA = Array.from(operations).find((op) => op.name === 'a'); + expect(operationA).toBeDefined(); + const commandHashA = operationA!.runner!.getConfigHash(); + // Should not contain --production but should contain other parameters + expect(commandHashA).not.toContain('--production'); + expect(commandHashA).toContain('--verbose'); + expect(commandHashA).toContain('--config'); + expect(commandHashA).toContain('--mode'); + expect(commandHashA).toContain('--tags'); + + // Verify that project 'b' has --verbose, --config, --mode, and --tags filtered out + const operationB = Array.from(operations).find((op) => op.name === 'b'); + expect(operationB).toBeDefined(); + const commandHashB = operationB!.runner!.getConfigHash(); + // Should contain --production but not the other parameters since they are filtered + expect(commandHashB).toContain('--production'); + expect(commandHashB).not.toContain('--verbose'); + expect(commandHashB).not.toContain('--config'); + expect(commandHashB).not.toContain('--mode'); + expect(commandHashB).not.toContain('--tags'); + + // All projects snapshot + expect(Array.from(operations, serializeOperation)).toMatchSnapshot(); + }); }); diff --git a/libraries/rush-lib/src/logic/operations/test/__snapshots__/ShellOperationRunnerPlugin.test.ts.snap b/libraries/rush-lib/src/logic/operations/test/__snapshots__/ShellOperationRunnerPlugin.test.ts.snap index f5990af4d43..346ce7e87b4 100644 --- a/libraries/rush-lib/src/logic/operations/test/__snapshots__/ShellOperationRunnerPlugin.test.ts.snap +++ b/libraries/rush-lib/src/logic/operations/test/__snapshots__/ShellOperationRunnerPlugin.test.ts.snap @@ -1,5 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`ShellOperationRunnerPlugin parameters should be filtered when parameterNamesToIgnore is specified 1`] = ` +Array [ + Object { + "commandToRun": "echo building a --verbose --config /path/to/config.json --mode prod --tags tag1 --tags tag2", + "name": "a", + }, + Object { + "commandToRun": "echo building b --production", + "name": "b", + }, +] +`; + exports[`ShellOperationRunnerPlugin shellCommand "echo custom shellCommand" should be set to commandToRun 1`] = ` Array [ Object { diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/config/rush-project.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/config/rush-project.json new file mode 100644 index 00000000000..103f06da2e0 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + "operationSettings": [ + { + "operationName": "build", + "parameterNamesToIgnore": ["--production"] + } + ] +} diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/package.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/package.json new file mode 100644 index 00000000000..d77de36cc7b --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/a/package.json @@ -0,0 +1,7 @@ +{ + "name": "a", + "version": "1.0.0", + "scripts": { + "build": "echo building a" + } +} diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/config/rush-project.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/config/rush-project.json new file mode 100644 index 00000000000..e6f27ab1857 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/config/rush-project.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + "operationSettings": [ + { + "operationName": "build", + "parameterNamesToIgnore": ["--verbose", "--config", "--mode", "--tags"] + } + ] +} diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/package.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/package.json new file mode 100644 index 00000000000..41a4b66e358 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/b/package.json @@ -0,0 +1,7 @@ +{ + "name": "b", + "version": "1.0.0", + "scripts": { + "build": "echo building b" + } +} diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/common/config/rush/command-line.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/common/config/rush/command-line.json new file mode 100644 index 00000000000..0f2df3fd189 --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/common/config/rush/command-line.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", + "commands": [], + "parameters": [ + { + "longName": "--production", + "description": "A production flag", + "parameterKind": "flag", + "associatedCommands": ["build"] + }, + { + "longName": "--verbose", + "description": "A verbose flag", + "parameterKind": "flag", + "associatedCommands": ["build"] + }, + { + "longName": "--config", + "description": "Config file path", + "parameterKind": "string", + "argumentName": "PATH", + "associatedCommands": ["build"] + }, + { + "longName": "--mode", + "description": "Build mode", + "parameterKind": "choice", + "alternatives": [ + { + "name": "dev", + "description": "Development mode" + }, + { + "name": "prod", + "description": "Production mode" + } + ], + "associatedCommands": ["build"] + }, + { + "longName": "--tags", + "description": "Build tags", + "parameterKind": "stringList", + "argumentName": "TAG", + "associatedCommands": ["build"] + } + ] +} diff --git a/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/rush.json b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/rush.json new file mode 100644 index 00000000000..529024b893c --- /dev/null +++ b/libraries/rush-lib/src/logic/test/parameterIgnoringRepo/rush.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", + "rushVersion": "5.162.0", + "pnpmVersion": "8.15.9", + "nodeSupportedVersionRange": ">=18.0.0", + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + }, + { + "packageName": "b", + "projectFolder": "b" + } + ] +} diff --git a/libraries/rush-lib/src/schemas/rush-project.schema.json b/libraries/rush-lib/src/schemas/rush-project.schema.json index 52883eaa6c1..acf1b20e5ae 100644 --- a/libraries/rush-lib/src/schemas/rush-project.schema.json +++ b/libraries/rush-lib/src/schemas/rush-project.schema.json @@ -110,6 +110,14 @@ "ignoreChangedProjectsOnlyFlag": { "type": "boolean", "description": "If true, this operation never be skipped by the `--changed-projects-only` flag. This is useful for projects that bundle code from other packages." + }, + "parameterNamesToIgnore": { + "type": "array", + "description": "An optional list of custom command-line parameter names that should be ignored when invoking the command for this operation. The parameter names should match the exact longName field values from the command-line.json parameters array (e.g., '--production', '--verbose'). This allows a project to opt out of parameters that don't affect its operation, preventing unnecessary cache invalidation for this operation and its consumers.", + "items": { + "type": "string" + }, + "uniqueItems": true } } }