diff --git a/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-17.json b/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-17.json new file mode 100644 index 00000000000..b449732f2bf --- /dev/null +++ b/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-17.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Fix telemetry for \"--changed-projects-only\" when toggled in watch mode.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-18.json b/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-18.json new file mode 100644 index 00000000000..5c3e07a4f99 --- /dev/null +++ b/common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-18.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "(rush-serve-plugin) Support websocket message to enable/disable operations.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 3f6728b1c9d..4f02d4f08c3 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -146,6 +146,7 @@ export class PhasedScriptAction extends BaseScriptAction { private readonly _alwaysInstall: boolean | undefined; private readonly _knownPhases: ReadonlyMap; private readonly _terminal: ITerminal; + private _changedProjectsOnly: boolean; private readonly _changedProjectsOnlyParameter: CommandLineFlagParameter | undefined; private readonly _selectionParameters: SelectionParameterSet; @@ -162,8 +163,6 @@ export class PhasedScriptAction extends BaseScriptAction { private readonly _debugBuildCacheIdsParameter: CommandLineFlagParameter; private readonly _includePhaseDeps: CommandLineFlagParameter | undefined; - private _changedProjectsOnly: boolean; - public constructor(options: IPhasedScriptActionOptions) { super(options); this._enableParallelism = options.enableParallelism; @@ -845,7 +844,7 @@ export class PhasedScriptAction extends BaseScriptAction { // Account for consumer relationships const executeOperationsContext: IExecuteOperationsContext = { ...initialCreateOperationsContext, - changedProjectsOnly: this._changedProjectsOnly, + changedProjectsOnly: !!this._changedProjectsOnly, isInitial: false, inputsSnapshot: state, projectsInUnknownState: changedProjects, @@ -961,6 +960,13 @@ export class PhasedScriptAction extends BaseScriptAction { countNoOp: 0 }; + const { _changedProjectsOnlyParameter: changedProjectsOnlyParameter } = this; + if (changedProjectsOnlyParameter) { + // Overwrite this value since we allow changing it at runtime. + extraData[changedProjectsOnlyParameter.scopedLongName ?? changedProjectsOnlyParameter.longName] = + this._changedProjectsOnly; + } + if (result) { const { operationResults } = result; diff --git a/libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts b/libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts index 77d2a479deb..44afe04b9be 100644 --- a/libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts @@ -21,6 +21,14 @@ const PLUGIN_NAME: 'PhasedOperationPlugin' = 'PhasedOperationPlugin'; export class PhasedOperationPlugin implements IPhasedCommandPlugin { public apply(hooks: PhasedCommandHooks): void { hooks.createOperations.tap(PLUGIN_NAME, createOperations); + // Configure operations later. + hooks.createOperations.tap( + { + name: `${PLUGIN_NAME}.Configure`, + stage: 1000 + }, + configureOperations + ); } } @@ -40,8 +48,6 @@ function createOperations( } } - configureOperations(existingOperations, context); - return existingOperations; // Binds phaseSelection, projectSelection, operations via closure @@ -88,7 +94,7 @@ function createOperations( } } -function configureOperations(operations: ReadonlySet, context: ICreateOperationsContext): void { +function configureOperations(operations: Set, context: ICreateOperationsContext): Set { const { changedProjectsOnly, projectsInUnknownState: changedProjects, @@ -149,6 +155,8 @@ function configureOperations(operations: ReadonlySet, context: ICreat operation.enabled &&= phaseSelection.has(associatedPhase) && projectSelection.has(associatedProject); } } + + return operations; } // Convert the [IPhase, RushConfigurationProject] into a value suitable for use as a Map key diff --git a/rush-plugins/rush-serve-plugin/src/api.types.ts b/rush-plugins/rush-serve-plugin/src/api.types.ts index 7509c7fa50b..2afcfc7a98d 100644 --- a/rush-plugins/rush-serve-plugin/src/api.types.ts +++ b/rush-plugins/rush-serve-plugin/src/api.types.ts @@ -151,7 +151,31 @@ export interface IWebSocketSyncCommandMessage { command: 'sync'; } +/** + * Message received from a WebSocket client to request invalidation of one or more operations. + */ +export interface IWebSocketInvalidateCommandMessage { + command: 'invalidate'; + operationNames: string[]; +} + +/** + * The set of possible operation enabled states. + */ +export type OperationEnabledState = 'never' | 'changed' | 'affected'; + +/** + * Message received from a WebSocket client to change the enabled states of operations. + */ +export interface IWebSocketSetEnabledStatesCommandMessage { + command: 'set-enabled-states'; + enabledStateByOperationName: Record; +} + /** * The set of possible messages received from a WebSocket client. */ -export type IWebSocketCommandMessage = IWebSocketSyncCommandMessage; +export type IWebSocketCommandMessage = + | IWebSocketSyncCommandMessage + | IWebSocketInvalidateCommandMessage + | IWebSocketSetEnabledStatesCommandMessage; diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index 97d395482c7..b9f3ea2d0d4 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -45,7 +45,8 @@ import type { ReadableOperationStatus, IWebSocketCommandMessage, IRushSessionInfo, - ILogFileURLs + ILogFileURLs, + OperationEnabledState } from './api.types'; export interface IPhasedCommandHandlerOptions { @@ -389,6 +390,51 @@ function tryEnableBuildStatusWebSocketServer( const { hooks } = command; + let invalidateOperation: ((operation: Operation, reason: string) => void) | undefined; + + const operationEnabledStates: Map = new Map(); + hooks.createOperations.tap( + { + name: PLUGIN_NAME, + stage: Infinity + }, + (operations: Set, context: ICreateOperationsContext) => { + const potentiallyAffectedOperations: Set = new Set(); + for (const operation of operations) { + const { associatedProject } = operation; + if (context.projectsInUnknownState.has(associatedProject)) { + potentiallyAffectedOperations.add(operation); + } + } + for (const operation of potentiallyAffectedOperations) { + for (const consumer of operation.consumers) { + potentiallyAffectedOperations.add(consumer); + } + + const { name } = operation; + const expectedState: OperationEnabledState | undefined = operationEnabledStates.get(name); + switch (expectedState) { + case 'affected': + operation.enabled = true; + break; + case 'never': + operation.enabled = false; + break; + case 'changed': + operation.enabled = context.projectsInUnknownState.has(operation.associatedProject); + break; + case undefined: + // Use the original value. + break; + } + } + + invalidateOperation = context.invalidateOperation; + + return operations; + } + ); + hooks.beforeExecuteOperations.tap( PLUGIN_NAME, (operationsToExecute: Map): void => { @@ -452,6 +498,27 @@ function tryEnableBuildStatusWebSocketServer( break; } + case 'set-enabled-states': { + const { enabledStateByOperationName } = parsedMessage; + for (const [name, state] of Object.entries(enabledStateByOperationName)) { + operationEnabledStates.set(name, state); + } + break; + } + + case 'invalidate': { + const { operationNames } = parsedMessage; + const operationNameSet: Set = new Set(operationNames); + if (invalidateOperation && operationStates) { + for (const operation of operationStates.keys()) { + if (operationNameSet.has(operation.name)) { + invalidateOperation(operation, 'WebSocket'); + } + } + } + break; + } + default: { // Unknown message. Ignore. }