Skip to content
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

[rush] Basic support of --watch for BulkScriptAction (build, rebuild, etc.) #2458

Merged
merged 19 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from 13 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
1 change: 1 addition & 0 deletions apps/rush-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@rushstack/ts-command-line": "workspace:*",
"@yarnpkg/lockfile": "~1.0.2",
"builtin-modules": "~3.1.0",
"chokidar": "~3.4.0",
dmichon-msft marked this conversation as resolved.
Show resolved Hide resolved
"cli-table": "~0.3.1",
"colors": "~1.2.1",
"git-repo-info": "~2.1.0",
Expand Down
206 changes: 173 additions & 33 deletions apps/rush-lib/src/cli/scriptActions/BulkScriptAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ import { PackageName } from '@rushstack/node-core-library';

import { Event } from '../../index';
import { SetupChecks } from '../../logic/SetupChecks';
import { TaskSelector } from '../../logic/TaskSelector';
import { Stopwatch } from '../../utilities/Stopwatch';
import { ITaskSelectorConstructor, TaskSelector } from '../../logic/TaskSelector';
import { Stopwatch, StopwatchState } from '../../utilities/Stopwatch';
import { BaseScriptAction, IBaseScriptActionOptions } from './BaseScriptAction';
import { TaskRunner } from '../../logic/taskRunner/TaskRunner';
import { TaskCollection } from '../../logic/taskRunner/TaskCollection';
import { ITaskRunnerOptions, TaskRunner } from '../../logic/taskRunner/TaskRunner';
import { Utilities } from '../../utilities/Utilities';
import { RushConstants } from '../../logic/RushConstants';
import { EnvironmentVariableNames } from '../../api/EnvironmentConfiguration';
import { LastLinkFlag, LastLinkFlagFactory } from '../../api/LastLinkFlag';
import { IRushConfigurationProjectJson, RushConfigurationProject } from '../../api/RushConfigurationProject';
import { BuildCacheConfiguration } from '../../api/BuildCacheConfiguration';
import { Selection } from '../../logic/Selection';
import { IProjectChangeResult, ProjectWatcher } from '../../logic/ProjectWatcher';

/**
* Constructor parameters for BulkScriptAction.
Expand All @@ -44,6 +44,14 @@ export interface IBulkScriptActionOptions extends IBaseScriptActionOptions {
commandToRun?: string;
}

interface IExecuteInternalOptions {
taskSelectorOptions: ITaskSelectorConstructor;
taskRunnerOptions: ITaskRunnerOptions;
stopwatch: Stopwatch;
ignoreHooks?: boolean;
terminal: Terminal;
}

/**
* This class implements bulk commands which are run individually for each project in the repo,
* possibly in parallel. The action executes a script found in the project's package.json file.
Expand All @@ -68,6 +76,7 @@ export class BulkScriptAction extends BaseScriptAction {
private _impactedByExceptProject!: CommandLineStringListParameter;
private _fromVersionPolicy!: CommandLineStringListParameter;
private _toVersionPolicy!: CommandLineStringListParameter;
private _watchParameter!: CommandLineFlagParameter;
private _verboseParameter!: CommandLineFlagParameter;
private _parallelismParameter: CommandLineStringParameter | undefined;
private _ignoreHooksParameter!: CommandLineFlagParameter;
Expand Down Expand Up @@ -158,7 +167,14 @@ export class BulkScriptAction extends BaseScriptAction {
Selection.expandAllConsumers(impactedByProjects)
);

const taskSelector: TaskSelector = new TaskSelector({
// If no projects selected, select everything.
if (selection.size === 0) {
for (const project of this.rushConfiguration.projects) {
selection.add(project);
}
}

const taskSelectorOptions: ITaskSelectorConstructor = {
rushConfiguration: this.rushConfiguration,
buildCacheConfiguration,
selection,
Expand All @@ -169,44 +185,105 @@ export class BulkScriptAction extends BaseScriptAction {
ignoreMissingScript: this._ignoreMissingScript,
ignoreDependencyOrder: this._ignoreDependencyOrder,
packageDepsFilename: Utilities.getPackageDepsFilenameForCommand(this._commandToRun)
});

// Register all tasks with the task collection
const taskCollection: TaskCollection = taskSelector.registerTasks();
};

const taskRunner: TaskRunner = new TaskRunner(taskCollection.getOrderedTasks(), {
const taskRunnerOptions: ITaskRunnerOptions = {
quietMode: isQuietMode,
parallelism: parallelism,
changedProjectsOnly: changedProjectsOnly,
allowWarningsInSuccessfulBuild: this._allowWarningsInSuccessfulBuild
});
};

try {
await taskRunner.executeAsync();
const executeOptions: IExecuteInternalOptions = {
taskSelectorOptions,
taskRunnerOptions,
stopwatch,
terminal
};

stopwatch.stop();
console.log(colors.green(`rush ${this.actionName} (${stopwatch.toString()})`));
if (this._watchParameter.value) {
await this.runWatch(executeOptions);
} else {
await this._runOnce(executeOptions);
}
}

this._doAfterTask(stopwatch, true);
} catch (error) {
stopwatch.stop();
/**
* Runs the command in watch mode. Fundamentally is a simple loop:
* 1) Wait for a change to one or more projects in the selection (skipped initially)
* 2) Invoke the command on the changed projects, and, if applicable, downstream projects
* 3) Goto (1)
*/
protected async runWatch(options: IExecuteInternalOptions): Promise<void> {
dmichon-msft marked this conversation as resolved.
Show resolved Hide resolved
const {
taskSelectorOptions: { selection: initialSelection },
stopwatch,
terminal
} = options;

const projectWatcher: ProjectWatcher = new ProjectWatcher({
debounceMilliseconds: 1000,
rushConfiguration: this.rushConfiguration,
selection: initialSelection
});

if (error instanceof AlreadyReportedError) {
console.log(`rush ${this.actionName} (${stopwatch.toString()})`);
} else {
if (error && error.message) {
if (this.parser.isDebug) {
console.log('Error: ' + error.stack);
} else {
console.log('Error: ' + error.message);
}
}
// Loop until Ctrl+C
// eslint-disable-next-line no-constant-condition
while (true) {
// Report so that the developer can always see that it is in watch mode.
terminal.writeLine(
`Watching for changes to ${initialSelection.size} ${
initialSelection.size === 1 ? 'project' : 'projects'
}. Press Ctrl+C to exit.`
);

// On the initial invocation, this promise will return immediately with the full set of projects
const change: IProjectChangeResult = await projectWatcher.waitForChange();

let selection: ReadonlySet<RushConfigurationProject> = change.changedProjects;

if (stopwatch.state === StopwatchState.Stopped) {
// Clear and reset the stopwatch so that we only report time from a single execution at a time
stopwatch.reset();
stopwatch.start();
}

console.log(colors.red(`rush ${this.actionName} - Errors! (${stopwatch.toString()})`));
terminal.writeLine(`Detected changes in ${selection.size} project${selection.size === 1 ? '' : 's'}:`);
const names: string[] = [...selection].map((x) => x.packageName).sort();
for (const name of names) {
terminal.writeLine(` ${colors.cyan(name)}`);
}

this._doAfterTask(stopwatch, false);
throw new AlreadyReportedError();
// If the command ignores dependency order, that means that only the changed projects should be affected
// That said, running watch for commands that ignore dependency order may have unexpected results
if (!this._ignoreDependencyOrder) {
selection = Selection.intersection(Selection.expandAllConsumers(selection), initialSelection);
}

const executeOptions: IExecuteInternalOptions = {
taskSelectorOptions: {
...options.taskSelectorOptions,
// Revise down the set of projects to execute the command on
selection,
// Pass the PackageChangeAnalyzer from the state differ to save a bit of overhead
packageChangeAnalyzer: change.state
},
taskRunnerOptions: options.taskRunnerOptions,
stopwatch,
// For now, don't run pre-build or post-build in watch mode
ignoreHooks: true,
terminal
};

try {
// Delegate the the underlying command, for only the projects that need reprocessing
await this._runOnce(executeOptions);
} catch (err) {
// In watch mode, we want to rebuild even if the original build failed.
if (!(err instanceof AlreadyReportedError)) {
throw err;
}
}
}
}

Expand Down Expand Up @@ -329,6 +406,18 @@ export class BulkScriptAction extends BaseScriptAction {
' For details, refer to the website article "Selecting subsets of projects".'
});

this._watchParameter = this.defineFlagParameter({
dmichon-msft marked this conversation as resolved.
Show resolved Hide resolved
parameterLongName: '--watch',
parameterShortName: '-w',
description:
'Normally Rush terminates after the command finishes. The "--watch" parameter will instead cause Rush' +
' to enter a loop where it watches the file system for changes to the selected projects.' +
' Whenever a change is detected, the command will be invoked again for the changed project and' +
' any selected projects that directly or indirectly depend on it.' +
' This parameter may be combined with "--changed-projects-only" to ignore dependent projects.' +
' For details, refer to the website article "Using watch mode".'
});

octogonz marked this conversation as resolved.
Show resolved Hide resolved
this._verboseParameter = this.defineFlagParameter({
parameterLongName: '--verbose',
parameterShortName: '-v',
Expand All @@ -339,8 +428,11 @@ export class BulkScriptAction extends BaseScriptAction {
parameterLongName: '--changed-projects-only',
parameterShortName: '-c',
description:
'If specified, the incremental build will only rebuild projects that have changed, ' +
'but not any projects that directly or indirectly depend on the changed package.'
'Normally the incremental build logic will rebuild changed projects as well as' +
' any projects that directly or indirectly depend on a changed project. Specify "--changed-projects-only"' +
' to ignore dependent projects, only rebuilding those projects whose files were changed.' +
' Note that this parameter is "unsafe"; it is up to the developer to ensure that the ignored projects' +
' are okay to ignore.'
});
}
this._ignoreHooksParameter = this.defineFlagParameter({
Expand All @@ -351,6 +443,54 @@ export class BulkScriptAction extends BaseScriptAction {
this.defineScriptParameters();
}

/**
* Runs a single invocation of the command
*/
private async _runOnce(options: IExecuteInternalOptions): Promise<void> {
const taskSelector: TaskSelector = new TaskSelector(options.taskSelectorOptions);

// Register all tasks with the task collection

const taskRunner: TaskRunner = new TaskRunner(
taskSelector.registerTasks().getOrderedTasks(),
options.taskRunnerOptions
);

const { ignoreHooks, stopwatch } = options;

try {
await taskRunner.executeAsync();

stopwatch.stop();
console.log(colors.green(`rush ${this.actionName} (${stopwatch.toString()})`));

if (!ignoreHooks) {
this._doAfterTask(stopwatch, true);
}
} catch (error) {
stopwatch.stop();

if (error instanceof AlreadyReportedError) {
console.log(`rush ${this.actionName} (${stopwatch.toString()})`);
} else {
if (error && error.message) {
if (this.parser.isDebug) {
console.log('Error: ' + error.stack);
} else {
console.log('Error: ' + error.message);
}
}

console.log(colors.red(`rush ${this.actionName} - Errors! (${stopwatch.toString()})`));
}

if (!ignoreHooks) {
this._doAfterTask(stopwatch, false);
}
throw new AlreadyReportedError();
}
}

private async _getProjectNames(): Promise<string[]> {
const unscopedNamesMap: Map<string, number> = new Map<string, number>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ exports[`CommandLineHelp prints the help for each action: build 1`] = `
"usage: rush build [-h] [-p COUNT] [-t PROJECT] [-T PROJECT] [-f PROJECT]
[-o PROJECT] [-i PROJECT] [-I PROJECT]
[--to-version-policy VERSION_POLICY_NAME]
[--from-version-policy VERSION_POLICY_NAME] [-v] [-c]
[--from-version-policy VERSION_POLICY_NAME] [-w] [-v] [-c]
[--ignore-hooks] [-s] [-m]


Expand Down Expand Up @@ -217,12 +217,27 @@ Optional arguments:
each of the projects belonging to VERSION_POLICY_NAME.
For details, refer to the website article \\"Selecting
subsets of projects\\".
-w, --watch Normally Rush terminates after the command finishes.
The \\"--watch\\" parameter will instead cause Rush to
enter a loop where it watches the file system for
changes to the selected projects. Whenever a change
is detected, the command will be invoked again for
the changed project and any selected projects that
directly or indirectly depend on it. This parameter
may be combined with \\"--changed-projects-only\\" to
ignore dependent projects. For details, refer to the
website article \\"Using watch mode\\".
-v, --verbose Display the logs during the build, rather than just
displaying the build status summary
-c, --changed-projects-only
If specified, the incremental build will only rebuild
projects that have changed, but not any projects that
directly or indirectly depend on the changed package.
Normally the incremental build logic will rebuild
changed projects as well as any projects that
directly or indirectly depend on a changed project.
Specify \\"--changed-projects-only\\" to ignore dependent
projects, only rebuilding those projects whose files
were changed. Note that this parameter is \\"unsafe\\";
it is up to the developer to ensure that the ignored
projects are okay to ignore.
--ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined
in rush.json. Make sure you know what you are
skipping.
Expand Down Expand Up @@ -349,8 +364,8 @@ exports[`CommandLineHelp prints the help for each action: import-strings 1`] = `
"usage: rush import-strings [-h] [-p COUNT] [-t PROJECT] [-T PROJECT]
[-f PROJECT] [-o PROJECT] [-i PROJECT] [-I PROJECT]
[--to-version-policy VERSION_POLICY_NAME]
[--from-version-policy VERSION_POLICY_NAME] [-v]
[--ignore-hooks]
[--from-version-policy VERSION_POLICY_NAME] [-w]
[-v] [--ignore-hooks]
[--locale {en-us,fr-fr,es-es,zh-cn}]


Expand Down Expand Up @@ -445,6 +460,16 @@ Optional arguments:
each of the projects belonging to VERSION_POLICY_NAME.
For details, refer to the website article \\"Selecting
subsets of projects\\".
-w, --watch Normally Rush terminates after the command finishes.
The \\"--watch\\" parameter will instead cause Rush to
enter a loop where it watches the file system for
changes to the selected projects. Whenever a change
is detected, the command will be invoked again for
the changed project and any selected projects that
directly or indirectly depend on it. This parameter
may be combined with \\"--changed-projects-only\\" to
ignore dependent projects. For details, refer to the
website article \\"Using watch mode\\".
-v, --verbose Display the logs during the build, rather than just
displaying the build status summary
--ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined
Expand Down Expand Up @@ -740,7 +765,7 @@ exports[`CommandLineHelp prints the help for each action: rebuild 1`] = `
"usage: rush rebuild [-h] [-p COUNT] [-t PROJECT] [-T PROJECT] [-f PROJECT]
[-o PROJECT] [-i PROJECT] [-I PROJECT]
[--to-version-policy VERSION_POLICY_NAME]
[--from-version-policy VERSION_POLICY_NAME] [-v]
[--from-version-policy VERSION_POLICY_NAME] [-w] [-v]
[--ignore-hooks] [-s] [-m]


Expand Down Expand Up @@ -841,6 +866,16 @@ Optional arguments:
each of the projects belonging to VERSION_POLICY_NAME.
For details, refer to the website article \\"Selecting
subsets of projects\\".
-w, --watch Normally Rush terminates after the command finishes.
The \\"--watch\\" parameter will instead cause Rush to
enter a loop where it watches the file system for
changes to the selected projects. Whenever a change
is detected, the command will be invoked again for
the changed project and any selected projects that
directly or indirectly depend on it. This parameter
may be combined with \\"--changed-projects-only\\" to
ignore dependent projects. For details, refer to the
website article \\"Using watch mode\\".
-v, --verbose Display the logs during the build, rather than just
displaying the build status summary
--ignore-hooks Skips execution of the \\"eventHooks\\" scripts defined
Expand Down
Loading