From 89511e1efbe8211af016e2c2b028ce0dc1bf1bcc Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 00:17:38 +0000 Subject: [PATCH 1/6] [rush] Fix telemetry for `--changed-projects-only` --- .../watch-changed-only_2025-04-17-00-17.json | 10 +++++++++ .../cli/scriptActions/PhasedScriptAction.ts | 21 ++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-17.json 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/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 3f6728b1c9d..2f958fc3679 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -162,8 +162,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; @@ -177,7 +175,6 @@ export class PhasedScriptAction extends BaseScriptAction { this._alwaysInstall = options.alwaysInstall; this._runsBeforeInstall = false; this._knownPhases = options.phases; - this._changedProjectsOnly = false; this.hooks = new PhasedCommandHooks(); @@ -334,6 +331,17 @@ export class PhasedScriptAction extends BaseScriptAction { } } + public get changedProjectsOnly(): boolean | undefined { + return this._changedProjectsOnlyParameter?.value; + } + + public set changedProjectsOnly(value: boolean) { + if (this._changedProjectsOnlyParameter) { + // Mutate the parameter so that the telemetry reflects the value. + (this._changedProjectsOnlyParameter as unknown as { _value?: boolean })._value = value; + } + } + public async runAsync(): Promise { if (this._alwaysInstall || this._installParameter?.value) { const { doBasicInstallAsync } = await import( @@ -431,7 +439,6 @@ export class PhasedScriptAction extends BaseScriptAction { const isQuietMode: boolean = !this._verboseParameter.value; const changedProjectsOnly: boolean = !!this._changedProjectsOnlyParameter?.value; - this._changedProjectsOnly = changedProjectsOnly; let buildCacheConfiguration: BuildCacheConfiguration | undefined; let cobuildConfiguration: CobuildConfiguration | undefined; @@ -677,7 +684,7 @@ export class PhasedScriptAction extends BaseScriptAction { ` Press <${quitKey}> to gracefully exit.`, ` Press <${toggleWatcherKey}> to ${isPaused ? 'resume' : 'pause'}.`, ` Press <${invalidateKey}> to invalidate all projects.`, - ` Press <${changedProjectsOnlyKey}> to ${this._changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).` + ` Press <${changedProjectsOnlyKey}> to ${this.changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this.changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).` ]; if (isPaused) { promptLines.push(` Press <${buildOnceKey}> to build once.`); @@ -721,7 +728,7 @@ export class PhasedScriptAction extends BaseScriptAction { } break; case changedProjectsOnlyKey: - this._changedProjectsOnly = !this._changedProjectsOnly; + this.changedProjectsOnly = !this.changedProjectsOnly; projectWatcher.rerenderStatus(); break; case shutdownProcessesKey: @@ -845,7 +852,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, From 9a14611643015267b8844b4cdac7970587ad7f09 Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 00:19:20 +0000 Subject: [PATCH 2/6] [rush-serve] Support enable/disable via websocket --- .../watch-changed-only_2025-04-17-00-18.json | 10 +++++++ .../logic/operations/PhasedOperationPlugin.ts | 14 +++++++--- .../rush-serve-plugin/src/api.types.ts | 12 ++++++++- .../src/phasedCommandHandler.ts | 27 +++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 common/changes/@microsoft/rush/watch-changed-only_2025-04-17-00-18.json 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/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..78bb401d8de 100644 --- a/rush-plugins/rush-serve-plugin/src/api.types.ts +++ b/rush-plugins/rush-serve-plugin/src/api.types.ts @@ -151,7 +151,17 @@ export interface IWebSocketSyncCommandMessage { command: 'sync'; } +/** + * 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 + | IWebSocketSetEnabledStatesCommandMessage; diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index 97d395482c7..f7431be474d 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -389,6 +389,25 @@ function tryEnableBuildStatusWebSocketServer( const { hooks } = command; + const operationEnabledStates: Map = new Map(); + hooks.createOperations.tap( + { + name: PLUGIN_NAME, + stage: 10 + }, + (operations: Set) => { + for (const operation of operations) { + const { name } = operation; + const expectedState: boolean | undefined = operationEnabledStates.get(name); + if (expectedState !== undefined) { + operation.enabled = expectedState; + } + } + + return operations; + } + ); + hooks.beforeExecuteOperations.tap( PLUGIN_NAME, (operationsToExecute: Map): void => { @@ -452,6 +471,14 @@ function tryEnableBuildStatusWebSocketServer( break; } + case 'set-enabled-states': { + const { enabledStateByOperationName } = parsedMessage; + for (const [name, enabled] of Object.entries(enabledStateByOperationName)) { + operationEnabledStates.set(name, enabled); + } + break; + } + default: { // Unknown message. Ignore. } From 74c4c534ee908111ae42d2ce95970e95553d0f79 Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 18:57:20 +0000 Subject: [PATCH 3/6] Stop mutating parameter --- .../cli/scriptActions/PhasedScriptAction.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 2f958fc3679..4a47ea71580 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; @@ -175,6 +176,7 @@ export class PhasedScriptAction extends BaseScriptAction { this._alwaysInstall = options.alwaysInstall; this._runsBeforeInstall = false; this._knownPhases = options.phases; + this._changedProjectsOnly = false; this.hooks = new PhasedCommandHooks(); @@ -331,17 +333,6 @@ export class PhasedScriptAction extends BaseScriptAction { } } - public get changedProjectsOnly(): boolean | undefined { - return this._changedProjectsOnlyParameter?.value; - } - - public set changedProjectsOnly(value: boolean) { - if (this._changedProjectsOnlyParameter) { - // Mutate the parameter so that the telemetry reflects the value. - (this._changedProjectsOnlyParameter as unknown as { _value?: boolean })._value = value; - } - } - public async runAsync(): Promise { if (this._alwaysInstall || this._installParameter?.value) { const { doBasicInstallAsync } = await import( @@ -684,7 +675,7 @@ export class PhasedScriptAction extends BaseScriptAction { ` Press <${quitKey}> to gracefully exit.`, ` Press <${toggleWatcherKey}> to ${isPaused ? 'resume' : 'pause'}.`, ` Press <${invalidateKey}> to invalidate all projects.`, - ` Press <${changedProjectsOnlyKey}> to ${this.changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this.changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).` + ` Press <${changedProjectsOnlyKey}> to ${this._changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).` ]; if (isPaused) { promptLines.push(` Press <${buildOnceKey}> to build once.`); @@ -728,7 +719,7 @@ export class PhasedScriptAction extends BaseScriptAction { } break; case changedProjectsOnlyKey: - this.changedProjectsOnly = !this.changedProjectsOnly; + this._changedProjectsOnly = !this._changedProjectsOnly; projectWatcher.rerenderStatus(); break; case shutdownProcessesKey: @@ -852,7 +843,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, @@ -968,6 +959,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; From 604174ac6ab9e51b7945f71cc9b0a8a274cb827c Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 20:02:32 +0000 Subject: [PATCH 4/6] [rush-serve] Allow setting enabled state to 'changed' --- .../rush-serve-plugin/src/api.types.ts | 7 +++- .../src/phasedCommandHandler.ts | 41 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/rush-plugins/rush-serve-plugin/src/api.types.ts b/rush-plugins/rush-serve-plugin/src/api.types.ts index 78bb401d8de..9abb6b69539 100644 --- a/rush-plugins/rush-serve-plugin/src/api.types.ts +++ b/rush-plugins/rush-serve-plugin/src/api.types.ts @@ -151,12 +151,17 @@ export interface IWebSocketSyncCommandMessage { command: 'sync'; } +/** + * 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; + enabledStateByOperationName: Record; } /** diff --git a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts index f7431be474d..239d147aef3 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,18 +390,40 @@ function tryEnableBuildStatusWebSocketServer( const { hooks } = command; - const operationEnabledStates: Map = new Map(); + const operationEnabledStates: Map = new Map(); hooks.createOperations.tap( { name: PLUGIN_NAME, - stage: 10 + stage: Infinity }, - (operations: Set) => { + (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: boolean | undefined = operationEnabledStates.get(name); - if (expectedState !== undefined) { - operation.enabled = expectedState; + 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; } } @@ -473,8 +496,8 @@ function tryEnableBuildStatusWebSocketServer( case 'set-enabled-states': { const { enabledStateByOperationName } = parsedMessage; - for (const [name, enabled] of Object.entries(enabledStateByOperationName)) { - operationEnabledStates.set(name, enabled); + for (const [name, state] of Object.entries(enabledStateByOperationName)) { + operationEnabledStates.set(name, state); } break; } From 60d052f9036d108a3226e1c77b1ab58787f5797b Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 20:44:47 +0000 Subject: [PATCH 5/6] [rush-serve] Support invalidation via websocket --- rush-plugins/rush-serve-plugin/src/api.types.ts | 9 +++++++++ .../src/phasedCommandHandler.ts | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/rush-plugins/rush-serve-plugin/src/api.types.ts b/rush-plugins/rush-serve-plugin/src/api.types.ts index 9abb6b69539..2afcfc7a98d 100644 --- a/rush-plugins/rush-serve-plugin/src/api.types.ts +++ b/rush-plugins/rush-serve-plugin/src/api.types.ts @@ -151,6 +151,14 @@ 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. */ @@ -169,4 +177,5 @@ export interface IWebSocketSetEnabledStatesCommandMessage { */ 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 239d147aef3..b9f3ea2d0d4 100644 --- a/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts +++ b/rush-plugins/rush-serve-plugin/src/phasedCommandHandler.ts @@ -390,6 +390,8 @@ function tryEnableBuildStatusWebSocketServer( const { hooks } = command; + let invalidateOperation: ((operation: Operation, reason: string) => void) | undefined; + const operationEnabledStates: Map = new Map(); hooks.createOperations.tap( { @@ -427,6 +429,8 @@ function tryEnableBuildStatusWebSocketServer( } } + invalidateOperation = context.invalidateOperation; + return operations; } ); @@ -502,6 +506,19 @@ function tryEnableBuildStatusWebSocketServer( 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. } From 12b268428d489bc7ff50768f5c29fe373605ea06 Mon Sep 17 00:00:00 2001 From: David Michon Date: Thu, 17 Apr 2025 20:45:13 +0000 Subject: [PATCH 6/6] fix setting changedProjectsOnly --- libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index 4a47ea71580..4f02d4f08c3 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -430,6 +430,7 @@ export class PhasedScriptAction extends BaseScriptAction { const isQuietMode: boolean = !this._verboseParameter.value; const changedProjectsOnly: boolean = !!this._changedProjectsOnlyParameter?.value; + this._changedProjectsOnly = changedProjectsOnly; let buildCacheConfiguration: BuildCacheConfiguration | undefined; let cobuildConfiguration: CobuildConfiguration | undefined;