Skip to content

Commit

Permalink
[rush] Look for :incremental suffixed scripts in watch mode (#4960)
Browse files Browse the repository at this point in the history
* [rush] Support `:incremental` scripts

* PR feedback

* Use explicit typeof check

---------

Co-authored-by: David Michon <dmichon-msft@users.noreply.github.com>
  • Loading branch information
dmichon-msft and dmichon-msft authored Oct 3, 2024
1 parent f03b7c9 commit f28e817
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Changes the behavior of phased commands in watch mode to, when running a phase `_phase:<name>` in all iterations after the first, prefer a script entry named `_phase:<name>:incremental` if such a script exists. The build cache will expect the outputs from the corresponding `_phase:<name>` script (with otherwise the same inputs) to be equivalent when looking for a cache hit.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin {
before: ShellOperationPluginName
},
async (operations: Set<Operation>, context: ICreateOperationsContext) => {
const { isWatch } = context;
const { isWatch, isInitial } = context;
if (!isWatch) {
return operations;
}

currentContext = context;

const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray<string> =
Expand All @@ -51,12 +55,22 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin {
continue;
}

const rawScript: string | undefined = project.packageJson.scripts?.[`${phase.name}:ipc`];
const { scripts } = project.packageJson;
if (!scripts) {
continue;
}

const { name: phaseName } = phase;

const rawScript: string | undefined =
(!isInitial ? scripts[`${phaseName}:incremental:ipc`] : undefined) ?? scripts[`${phaseName}:ipc`];

if (!rawScript) {
continue;
}

const commandToRun: string = formatCommand(rawScript, getCustomParameterValuesForPhase(phase));
const customParameterValues: ReadonlyArray<string> = getCustomParameterValuesForPhase(phase);
const commandToRun: string = formatCommand(rawScript, customParameterValues);

const operationName: string = getDisplayName(phase, project);
let maybeIpcOperationRunner: IPCOperationRunner | undefined = runnerCache.get(operationName);
Expand All @@ -66,7 +80,7 @@ export class IPCOperationRunnerPlugin implements IPhasedCommandPlugin {
project,
name: operationName,
shellCommand: commandToRun,
persist: isWatch,
persist: true,
requestRun: (requestor?: string) => {
const operationState: IOperationExecutionResult | undefined =
operationStatesByRunner.get(ipcOperationRunner);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ import { NullOperationRunner } from './NullOperationRunner';
import { Operation } from './Operation';
import { OperationStatus } from './OperationStatus';
import {
formatCommand,
getCustomParameterValuesByPhase,
getDisplayName,
getScriptToRun,
initializeShellOperationRunner
} from './ShellOperationRunnerPlugin';

Expand Down Expand Up @@ -129,23 +127,21 @@ function spliceShards(existingOperations: Set<Operation>, context: ICreateOperat
`--shard-count="${shards}"`
];

const rawCommandToRun: string | undefined = getScriptToRun(project, phase.name, phase.shellCommand);

const commandToRun: string | undefined = rawCommandToRun
? formatCommand(rawCommandToRun, collatorParameters)
: undefined;
const { scripts } = project.packageJson;
const commandToRun: string | undefined = phase.shellCommand ?? scripts?.[phase.name];

operation.logFilenameIdentifier = `${baseLogFilenameIdentifier}_collate`;
operation.runner = initializeShellOperationRunner({
phase,
project,
displayName: collatorDisplayName,
rushConfiguration,
commandToRun: commandToRun
commandToRun,
customParameterValues: collatorParameters
});

const shardOperationName: string = `${phase.name}:shard`;
const baseCommand: string | undefined = getScriptToRun(project, shardOperationName, undefined);
const baseCommand: string | undefined = scripts?.[shardOperationName];
if (baseCommand === undefined) {
throw new Error(
`The project '${project.packageName}' does not define a '${phase.name}:shard' command in the 'scripts' section of its package.json`
Expand Down Expand Up @@ -205,14 +201,11 @@ function spliceShards(existingOperations: Set<Operation>, context: ICreateOperat

const shardDisplayName: string = `${getDisplayName(phase, project)} - shard ${shard}/${shards}`;

const shardedCommandToRun: string | undefined = baseCommand
? formatCommand(baseCommand, shardedParameters)
: undefined;

shardOperation.runner = initializeShellOperationRunner({
phase,
project,
commandToRun: shardedCommandToRun,
commandToRun: baseCommand,
customParameterValues: shardedParameters,
displayName: shardDisplayName,
rushConfiguration
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface IOperationRunnerOptions {
rushProject: RushConfigurationProject;
rushConfiguration: RushConfiguration;
commandToRun: string;
commandForHash: string;
displayName: string;
phase: IPhase;
environment?: IEnvironment;
Expand All @@ -38,6 +39,7 @@ export class ShellOperationRunner implements IOperationRunner {
public readonly warningsAreAllowed: boolean;

private readonly _commandToRun: string;
private readonly _commandForHash: string;

private readonly _rushProject: RushConfigurationProject;
private readonly _rushConfiguration: RushConfiguration;
Expand All @@ -53,6 +55,7 @@ export class ShellOperationRunner implements IOperationRunner {
this._rushProject = options.rushProject;
this._rushConfiguration = options.rushConfiguration;
this._commandToRun = options.commandToRun;
this._commandForHash = options.commandForHash;
this._environment = options.environment;
}

Expand All @@ -65,7 +68,7 @@ export class ShellOperationRunner implements IOperationRunner {
}

public getConfigHash(): string {
return this._commandToRun;
return this._commandForHash;
}

private async _executeAsync(context: IOperationRunnerContext): Promise<OperationStatus> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function createShellOperations(
operations: Set<Operation>,
context: ICreateOperationsContext
): Set<Operation> {
const { rushConfiguration } = context;
const { rushConfiguration, isInitial } = context;

const getCustomParameterValuesForPhase: (phase: IPhase) => ReadonlyArray<string> =
getCustomParameterValuesByPhase();
Expand All @@ -44,17 +44,27 @@ function createShellOperations(
const customParameterValues: ReadonlyArray<string> = getCustomParameterValuesForPhase(phase);

const displayName: string = getDisplayName(phase, project);
const { name: phaseName, shellCommand } = phase;

const rawCommandToRun: string | undefined = getScriptToRun(project, phase.name, phase.shellCommand);
const { scripts } = project.packageJson;

// This is the command that will be used to identify the cache entry for this operation
const commandForHash: string | undefined = shellCommand ?? scripts?.[phaseName];

// For execution of non-initial runs, prefer the `:incremental` script if it exists.
// However, the `shellCommand` value still takes precedence per the spec for that feature.
const commandToRun: string | undefined =
rawCommandToRun !== undefined ? formatCommand(rawCommandToRun, customParameterValues) : undefined;
shellCommand ??
(!isInitial ? scripts?.[`${phaseName}:incremental`] : undefined) ??
scripts?.[phaseName];

operation.runner = initializeShellOperationRunner({
phase,
project,
displayName,
commandForHash,
commandToRun,
customParameterValues,
rushConfiguration
});
}
Expand All @@ -69,18 +79,28 @@ export function initializeShellOperationRunner(options: {
displayName: string;
rushConfiguration: RushConfiguration;
commandToRun: string | undefined;
commandForHash?: string;
customParameterValues: ReadonlyArray<string>;
}): IOperationRunner {
const { phase, project, rushConfiguration, commandToRun, displayName } = options;
const { phase, project, commandToRun: rawCommandToRun, displayName } = options;

if (commandToRun === undefined && phase.missingScriptBehavior === 'error') {
if (typeof rawCommandToRun !== 'string' && phase.missingScriptBehavior === 'error') {
throw new Error(
`The project '${project.packageName}' does not define a '${phase.name}' command in the 'scripts' section of its package.json`
);
}

if (commandToRun) {
if (rawCommandToRun) {
const { rushConfiguration, commandForHash: rawCommandForHash } = options;

const commandToRun: string = formatCommand(rawCommandToRun, options.customParameterValues);
const commandForHash: string = rawCommandForHash
? formatCommand(rawCommandForHash, options.customParameterValues)
: commandToRun;

return new ShellOperationRunner({
commandToRun: commandToRun || '',
commandToRun,
commandForHash,
displayName,
phase,
rushConfiguration,
Expand All @@ -96,22 +116,6 @@ export function initializeShellOperationRunner(options: {
}
}

export function getScriptToRun(
rushProject: RushConfigurationProject,
commandToRun: string,
shellCommand: string | undefined
): string | undefined {
const { scripts } = rushProject.packageJson;

const rawCommand: string | undefined | null = shellCommand ?? scripts?.[commandToRun];

if (rawCommand === undefined || rawCommand === null) {
return undefined;
}

return rawCommand;
}

/**
* Memoizer for custom parameter values by phase
* @returns A function that returns the custom parameter values for a given phase
Expand Down
2 changes: 1 addition & 1 deletion libraries/rush-lib/src/schemas/command-line.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
},
"watchPhases": {
"title": "Watch Phases",
"description": "List *exactly* the phases that should be run in watch mode for this command. If this property is specified and non-empty, after the phases defined in the \"phases\" property run, a file watcher will be started to watch projects for changes, and will run the phases listed in this property on changed projects.",
"description": "List *exactly* the phases that should be run in watch mode for this command. If this property is specified and non-empty, after the phases defined in the \"phases\" property run, a file watcher will be started to watch projects for changes, and will run the phases listed in this property on changed projects. Rush will prefer scripts named \"${phaseName}:incremental\" over \"${phaseName}\" for every iteration after the first, so you can reuse the same phase name but define different scripts, e.g. to not clean on incremental runs.",
"type": "array",
"items": {
"type": "string"
Expand Down

0 comments on commit f28e817

Please sign in to comment.