Skip to content
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": "Support the `--changed-projects-only` flag in watch mode and allow it to be toggled between iterations.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
2 changes: 2 additions & 0 deletions common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export interface IConfigurationEnvironmentVariable {
// @alpha
export interface ICreateOperationsContext {
readonly buildCacheConfiguration: BuildCacheConfiguration | undefined;
readonly changedProjectsOnly: boolean;
readonly cobuildConfiguration: CobuildConfiguration | undefined;
readonly customParameters: ReadonlyMap<string, CommandLineParameter>;
readonly includePhaseDeps: boolean;
Expand Down Expand Up @@ -663,6 +664,7 @@ export interface IOperationSettings {
dependsOnAdditionalFiles?: string[];
dependsOnEnvVars?: string[];
disableBuildCacheForOperation?: boolean;
ignoreChangedProjectsOnlyFlag?: boolean;
operationName: string;
outputFolderNames?: string[];
sharding?: IRushPhaseSharding;
Expand Down
5 changes: 5 additions & 0 deletions libraries/rush-lib/src/api/RushProjectConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export interface IOperationSettings {
* If true, this operation can use cobuilds for orchestration without restoring build cache entries.
*/
allowCobuildWithoutCache?: boolean;

/**
* If true, this operation will never be skipped by the `--changed-projects-only` flag.
*/
ignoreChangedProjectsOnlyFlag?: boolean;
}

interface IOldRushProjectJson {
Expand Down
24 changes: 18 additions & 6 deletions libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
private readonly _knownPhases: ReadonlyMap<string, IPhase>;
private readonly _terminal: ITerminal;

private readonly _changedProjectsOnly: CommandLineFlagParameter | undefined;
private readonly _changedProjectsOnlyParameter: CommandLineFlagParameter | undefined;
private readonly _selectionParameters: SelectionParameterSet;
private readonly _verboseParameter: CommandLineFlagParameter;
private readonly _parallelismParameter: CommandLineStringParameter | undefined;
Expand All @@ -162,6 +162,8 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
private readonly _debugBuildCacheIdsParameter: CommandLineFlagParameter;
private readonly _includePhaseDeps: CommandLineFlagParameter | undefined;

private _changedProjectsOnly: boolean;

public constructor(options: IPhasedScriptActionOptions) {
super(options);
this._enableParallelism = options.enableParallelism;
Expand All @@ -175,6 +177,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
this._alwaysInstall = options.alwaysInstall;
this._runsBeforeInstall = false;
this._knownPhases = options.phases;
this._changedProjectsOnly = false;

this.hooks = new PhasedCommandHooks();

Expand Down Expand Up @@ -243,7 +246,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
`Using "--impacted-by A --include-phase-deps" avoids that work by performing "_phase:test" only for downstream projects.`
});

this._changedProjectsOnly = this._isIncrementalBuildAllowed
this._changedProjectsOnlyParameter = this._isIncrementalBuildAllowed
? this.defineFlagParameter({
parameterLongName: '--changed-projects-only',
parameterShortName: '-c',
Expand Down Expand Up @@ -427,7 +430,8 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {

const isQuietMode: boolean = !this._verboseParameter.value;

const changedProjectsOnly: boolean = !!this._changedProjectsOnly?.value;
const changedProjectsOnly: boolean = !!this._changedProjectsOnlyParameter?.value;
this._changedProjectsOnly = changedProjectsOnly;

let buildCacheConfiguration: BuildCacheConfiguration | undefined;
let cobuildConfiguration: CobuildConfiguration | undefined;
Expand Down Expand Up @@ -528,6 +532,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {

const initialCreateOperationsContext: ICreateOperationsContext = {
buildCacheConfiguration,
changedProjectsOnly,
cobuildConfiguration,
customParameters: customParametersByName,
isIncrementalBuildAllowed: this._isIncrementalBuildAllowed,
Expand Down Expand Up @@ -658,19 +663,21 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
projectWatcher: ProjectWatcher,
abortController: AbortController
): void {
const toggleWatcherKey: 'w' = 'w';
const buildOnceKey: 'b' = 'b';
const changedProjectsOnlyKey: 'c' = 'c';
const invalidateKey: 'i' = 'i';
const shutdownProcessesKey: 'x' = 'x';
const quitKey: 'q' = 'q';
const toggleWatcherKey: 'w' = 'w';
const shutdownProcessesKey: 'x' = 'x';

const terminal: ITerminal = this._terminal;

projectWatcher.setPromptGenerator((isPaused: boolean) => {
const promptLines: string[] = [
` Press <${quitKey}> to gracefully exit.`,
` Press <${toggleWatcherKey}> to ${isPaused ? 'resume' : 'pause'}.`,
` Press <${invalidateKey}> to invalidate all projects.`
` Press <${invalidateKey}> to invalidate all projects.`,
` Press <${changedProjectsOnlyKey}> to ${this._changedProjectsOnly ? 'disable' : 'enable'} changed-projects-only mode (${this._changedProjectsOnly ? 'ENABLED' : 'DISABLED'}).`
];
if (isPaused) {
promptLines.push(` Press <${buildOnceKey}> to build once.`);
Expand Down Expand Up @@ -713,6 +720,10 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
projectWatcher.resume();
}
break;
case changedProjectsOnlyKey:
this._changedProjectsOnly = !this._changedProjectsOnly;
projectWatcher.rerenderStatus();
break;
case shutdownProcessesKey:
projectWatcher.clearStatus();
terminal.writeLine(`Shutting down long-lived child processes...`);
Expand Down Expand Up @@ -834,6 +845,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
// Account for consumer relationships
const executeOperationsContext: IExecuteOperationsContext = {
...initialCreateOperationsContext,
changedProjectsOnly: this._changedProjectsOnly,
isInitial: false,
inputsSnapshot: state,
projectsInUnknownState: changedProjects,
Expand Down
6 changes: 6 additions & 0 deletions libraries/rush-lib/src/logic/ProjectWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class ProjectWatcher {
private _onAbort: undefined | (() => void);
private _getPromptLines: undefined | IPromptGeneratorFunction;

private _lastStatus: string | undefined;
private _renderedStatusLines: number;

public isPaused: boolean = false;
Expand Down Expand Up @@ -140,6 +141,10 @@ export class ProjectWatcher {
this._renderedStatusLines = 0;
}

public rerenderStatus(): void {
this._setStatus(this._lastStatus ?? 'Waiting for changes...');
}

public setPromptGenerator(promptGenerator: IPromptGeneratorFunction): void {
this._getPromptLines = promptGenerator;
}
Expand Down Expand Up @@ -427,6 +432,7 @@ export class ProjectWatcher {
readline.clearScreenDown(process.stdout);
}
this._renderedStatusLines = statusLines.length;
this._lastStatus = status;

this._terminal.writeLine(Colorize.bold(Colorize.cyan(statusLines.join('\n'))));
}
Expand Down
122 changes: 66 additions & 56 deletions libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,72 +28,19 @@ function createOperations(
existingOperations: Set<Operation>,
context: ICreateOperationsContext
): Set<Operation> {
const {
projectsInUnknownState: changedProjects,
phaseOriginal,
phaseSelection,
projectSelection,
projectConfigurations,
includePhaseDeps,
isInitial
} = context;
const { phaseSelection, projectSelection, projectConfigurations } = context;

const operations: Map<string, Operation> = new Map();

// Create tasks for selected phases and projects
// This also creates the minimal set of dependencies needed
for (const phase of phaseOriginal) {
for (const phase of phaseSelection) {
for (const project of projectSelection) {
getOrCreateOperation(phase, project);
}
}

// Grab all operations that were explicitly requested.
const operationsWithWork: Set<Operation> = new Set();
for (const operation of existingOperations) {
const { associatedPhase, associatedProject } = operation;
if (!associatedPhase || !associatedProject) {
// Fix this when these are required properties.
continue;
}

if (phaseSelection.has(associatedPhase) && changedProjects.has(associatedProject)) {
operationsWithWork.add(operation);
}
}

// Add all operations that are selected that depend on the explicitly requested operations.
// This will mostly be relevant during watch; in initial runs it should not add any new operations.
for (const operation of operationsWithWork) {
for (const consumer of operation.consumers) {
operationsWithWork.add(consumer);
}
}

if (includePhaseDeps && isInitial) {
// Add all operations that are dependencies of the operations already scheduled.
for (const operation of operationsWithWork) {
for (const dependency of operation.dependencies) {
operationsWithWork.add(dependency);
}
}
}

for (const operation of existingOperations) {
// Enable exactly the set of operations that are requested.
operation.enabled &&= operationsWithWork.has(operation);

if (!includePhaseDeps || !isInitial) {
const { associatedPhase, associatedProject } = operation;
if (!associatedPhase || !associatedProject) {
// Fix this when these are required properties.
continue;
}

// This filter makes the "unsafe" selections happen.
operation.enabled &&= phaseSelection.has(associatedPhase) && projectSelection.has(associatedProject);
}
}
configureOperations(existingOperations, context);

return existingOperations;

Expand Down Expand Up @@ -141,6 +88,69 @@ function createOperations(
}
}

function configureOperations(operations: ReadonlySet<Operation>, context: ICreateOperationsContext): void {
const {
changedProjectsOnly,
projectsInUnknownState: changedProjects,
phaseOriginal,
phaseSelection,
projectSelection,
includePhaseDeps,
isInitial
} = context;

// Grab all operations that were explicitly requested.
const operationsWithWork: Set<Operation> = new Set();
for (const operation of operations) {
const { associatedPhase, associatedProject } = operation;
if (phaseOriginal.has(associatedPhase) && changedProjects.has(associatedProject)) {
operationsWithWork.add(operation);
}
}

if (!isInitial && changedProjectsOnly) {
const potentiallyAffectedOperations: Set<Operation> = new Set(operationsWithWork);
for (const operation of potentiallyAffectedOperations) {
if (operation.settings?.ignoreChangedProjectsOnlyFlag) {
operationsWithWork.add(operation);
}

for (const consumer of operation.consumers) {
potentiallyAffectedOperations.add(consumer);
}
}
} else {
// Add all operations that are selected that depend on the explicitly requested operations.
// This will mostly be relevant during watch; in initial runs it should not add any new operations.
for (const operation of operationsWithWork) {
for (const consumer of operation.consumers) {
operationsWithWork.add(consumer);
}
}
}

if (includePhaseDeps) {
// Add all operations that are dependencies of the operations already scheduled.
for (const operation of operationsWithWork) {
for (const dependency of operation.dependencies) {
operationsWithWork.add(dependency);
}
}
}

for (const operation of operations) {
// Enable exactly the set of operations that are requested.
operation.enabled &&= operationsWithWork.has(operation);

if (!includePhaseDeps || !isInitial) {
const { associatedPhase, associatedProject } = operation;

// This filter makes the "unsafe" selections happen.
operation.enabled &&= phaseSelection.has(associatedPhase) && projectSelection.has(associatedProject);
}
}
}

// Convert the [IPhase, RushConfigurationProject] into a value suitable for use as a Map key
function getOperationKey(phase: IPhase, project: RushConfigurationProject): string {
return `${project.packageName};${phase.name}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ export interface ICreateOperationsContext {
* The configuration for the build cache, if the feature is enabled.
*/
readonly buildCacheConfiguration: BuildCacheConfiguration | undefined;
/**
* If true, for an incremental build, Rush will only include projects with immediate changes or projects with no consumers.
* @remarks
* This is an optimization that may produce invalid outputs if some of the intervening projects are impacted by the changes.
*/
readonly changedProjectsOnly: boolean;
/**
* The configuration for the cobuild, if cobuild feature and build cache feature are both enabled.
*/
Expand Down
4 changes: 4 additions & 0 deletions libraries/rush-lib/src/schemas/rush-project.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
"allowCobuildWithoutCache": {
"type": "boolean",
"description": "If true, this operation will not need to use the build cache to leverage cobuilds"
},
"ignoreChangedProjectsOnlyFlag": {
"type": "boolean",
"description": "If true, this operation never be skipped by the `--changed-projects-only` flag. This is useful for projects that bundle code from other packages."
}
}
}
Expand Down
Loading