Skip to content

[rush] Look for :incremental suffixed scripts in watch mode #4960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading