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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/build-tests/eslint-bulk-suppressions-test-legacy](./build-tests/eslint-bulk-suppressions-test-legacy/) | Sample code to test eslint bulk suppressions for versions of eslint < 8.57.0 |
| [/build-tests/hashed-folder-copy-plugin-webpack5-test](./build-tests/hashed-folder-copy-plugin-webpack5-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 5. NOTE - THIS TEST IS CURRENTLY EXPECTED TO BE BROKEN |
| [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft |
| [/build-tests/heft-example-lifecycle-plugin](./build-tests/heft-example-lifecycle-plugin/) | This is an example heft plugin for testing the lifecycle hooks |
| [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins |
| [/build-tests/heft-example-plugin-02](./build-tests/heft-example-plugin-02/) | This is an example heft plugin that taps the hooks exposed from heft-example-plugin-01 |
| [/build-tests/heft-fastify-test](./build-tests/heft-fastify-test/) | This project tests Heft support for the Fastify framework for Node.js services |
Expand Down
2 changes: 1 addition & 1 deletion apps/heft/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"license": "MIT",
"scripts": {
"build": "heft build --clean",
"start": "heft test --clean --watch",
"start": "heft build-watch --clean",
"_phase:build": "heft run --only build -- --clean",
"_phase:test": "heft run --only test -- --clean"
},
Expand Down
129 changes: 110 additions & 19 deletions apps/heft/src/cli/HeftActionRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type IWatchLoopState,
Operation,
OperationExecutionManager,
OperationGroupRecord,
OperationStatus,
WatchLoop
} from '@rushstack/operation-graph';
Expand All @@ -28,7 +29,7 @@ import type { MetricsCollector } from '../metrics/MetricsCollector';
import { HeftParameterManager } from '../pluginFramework/HeftParameterManager';
import { TaskOperationRunner } from '../operations/runners/TaskOperationRunner';
import { PhaseOperationRunner } from '../operations/runners/PhaseOperationRunner';
import type { HeftPhase } from '../pluginFramework/HeftPhase';
import type { IHeftPhase, HeftPhase } from '../pluginFramework/HeftPhase';
import type { IHeftAction, IHeftActionOptions } from './actions/IHeftAction';
import type {
IHeftLifecycleCleanHookOptions,
Expand All @@ -37,14 +38,31 @@ import type {
IHeftLifecycleToolStartHookOptions
} from '../pluginFramework/HeftLifecycleSession';
import type { HeftLifecycle } from '../pluginFramework/HeftLifecycle';
import type { HeftTask } from '../pluginFramework/HeftTask';
import type { IHeftTask, HeftTask } from '../pluginFramework/HeftTask';
import { deleteFilesAsync, type IDeleteOperation } from '../plugins/DeleteFilesPlugin';
import { Constants } from '../utilities/Constants';

export interface IHeftActionRunnerOptions extends IHeftActionOptions {
action: IHeftAction;
}

/**
* Metadata for an operation that represents a task.
* @public
*/
export interface IHeftTaskOperationMetadata {
task: IHeftTask;
phase: IHeftPhase;
}

/**
* Metadata for an operation that represents a phase.
* @public
*/
export interface IHeftPhaseOperationMetadata {
phase: IHeftPhase;
}

export function initializeHeft(
heftConfiguration: HeftConfiguration,
terminal: ITerminal,
Expand Down Expand Up @@ -291,9 +309,13 @@ export class HeftActionRunner {

initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose);

const operations: ReadonlySet<Operation> = this._generateOperations();
const operations: ReadonlySet<Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>> =
this._generateOperations();

const executionManager: OperationExecutionManager = new OperationExecutionManager(operations);
const executionManager: OperationExecutionManager<
IHeftTaskOperationMetadata,
IHeftPhaseOperationMetadata
> = new OperationExecutionManager(operations);

const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal);

Expand Down Expand Up @@ -346,20 +368,52 @@ export class HeftActionRunner {
}

private async _executeOnceAsync(
executionManager: OperationExecutionManager,
executionManager: OperationExecutionManager<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>,
abortSignal: AbortSignal,
requestRun?: (requestor?: string) => void
): Promise<OperationStatus> {
const { taskStart, taskFinish, phaseStart, phaseFinish } = this._internalHeftSession.lifecycle.hooks;
// Record this as the start of task execution.
this._metricsCollector.setStartTime();
// Execute the action operations
return await runWithLoggingAsync(
() => {
const operationExecutionManagerOptions: IOperationExecutionOptions = {
const operationExecutionManagerOptions: IOperationExecutionOptions<
IHeftTaskOperationMetadata,
IHeftPhaseOperationMetadata
> = {
terminal: this._terminal,
parallelism: this._parallelism,
abortSignal,
requestRun
requestRun,
beforeExecuteOperationAsync: async (
operation: Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
) => {
if (taskStart.isUsed()) {
await taskStart.promise({ operation });
}
},
afterExecuteOperationAsync: async (
operation: Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
) => {
if (taskFinish.isUsed()) {
await taskFinish.promise({ operation });
}
},
beforeExecuteOperationGroupAsync: async (
operationGroup: OperationGroupRecord<IHeftPhaseOperationMetadata>
) => {
if (operationGroup.metadata.phase && phaseStart.isUsed()) {
await phaseStart.promise({ operation: operationGroup });
}
},
afterExecuteOperationGroupAsync: async (
operationGroup: OperationGroupRecord<IHeftPhaseOperationMetadata>
) => {
if (operationGroup.metadata.phase && phaseFinish.isUsed()) {
await phaseFinish.promise({ operation: operationGroup });
}
}
};

return executionManager.executeAsync(operationExecutionManagerOptions);
Expand All @@ -373,10 +427,14 @@ export class HeftActionRunner {
);
}

private _generateOperations(): Set<Operation> {
private _generateOperations(): Set<Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>> {
const { selectedPhases } = this._action;

const operations: Map<string, Operation> = new Map();
const operations: Map<
string,
Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
> = new Map();
const operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>> = new Map();
const internalHeftSession: InternalHeftSession = this._internalHeftSession;

let hasWarnedAboutSkippedPhases: boolean = false;
Expand All @@ -399,18 +457,28 @@ export class HeftActionRunner {
}

// Create operation for the phase start node
const phaseOperation: Operation = _getOrCreatePhaseOperation(internalHeftSession, phase, operations);
const phaseOperation: Operation = _getOrCreatePhaseOperation(
internalHeftSession,
phase,
operations,
operationGroups
);

// Create operations for each task
for (const task of phase.tasks) {
const taskOperation: Operation = _getOrCreateTaskOperation(internalHeftSession, task, operations);
const taskOperation: Operation = _getOrCreateTaskOperation(
internalHeftSession,
task,
operations,
operationGroups
);
// Set the phase operation as a dependency of the task operation to ensure the phase operation runs first
taskOperation.addDependency(phaseOperation);

// Set all dependency tasks as dependencies of the task operation
for (const dependencyTask of task.dependencyTasks) {
taskOperation.addDependency(
_getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations)
_getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations, operationGroups)
);
}

Expand All @@ -422,7 +490,8 @@ export class HeftActionRunner {
const consumingPhaseOperation: Operation = _getOrCreatePhaseOperation(
internalHeftSession,
consumingPhase,
operations
operations,
operationGroups
);
consumingPhaseOperation.addDependency(taskOperation);
// This is purely to simplify the reported graph for phase circularities
Expand All @@ -440,15 +509,24 @@ function _getOrCreatePhaseOperation(
this: void,
internalHeftSession: InternalHeftSession,
phase: HeftPhase,
operations: Map<string, Operation>
operations: Map<string, Operation>,
operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>>
): Operation {
const key: string = phase.phaseName;

let operation: Operation | undefined = operations.get(key);
if (!operation) {
let group: OperationGroupRecord<IHeftPhaseOperationMetadata> | undefined = operationGroups.get(
phase.phaseName
);
if (!group) {
group = new OperationGroupRecord(phase.phaseName, { phase });
operationGroups.set(phase.phaseName, group);
}
// Only create the operation. Dependencies are hooked up separately
operation = new Operation({
groupName: phase.phaseName,
group,
name: phase.phaseName,
runner: new PhaseOperationRunner({ phase, internalHeftSession })
});
operations.set(key, operation);
Expand All @@ -460,18 +538,31 @@ function _getOrCreateTaskOperation(
this: void,
internalHeftSession: InternalHeftSession,
task: HeftTask,
operations: Map<string, Operation>
operations: Map<string, Operation>,
operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>>
): Operation {
const key: string = `${task.parentPhase.phaseName}.${task.taskName}`;

let operation: Operation | undefined = operations.get(key);
let operation: Operation<IHeftTaskOperationMetadata> | undefined = operations.get(
key
) as Operation<IHeftTaskOperationMetadata>;
if (!operation) {
const group: OperationGroupRecord<IHeftPhaseOperationMetadata> | undefined = operationGroups.get(
task.parentPhase.phaseName
);
if (!group) {
throw new InternalError(
`Task ${task.taskName} in phase ${task.parentPhase.phaseName} has no group. This should not happen.`
);
}
operation = new Operation({
groupName: task.parentPhase.phaseName,
group,
runner: new TaskOperationRunner({
internalHeftSession,
task
})
}),
name: task.taskName,
metadata: { task, phase: task.parentPhase }
});
operations.set(key, operation);
}
Expand Down
12 changes: 11 additions & 1 deletion apps/heft/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export type {
IHeftLifecycleHooks,
IHeftLifecycleCleanHookOptions,
IHeftLifecycleToolStartHookOptions,
IHeftLifecycleToolFinishHookOptions
IHeftLifecycleToolFinishHookOptions,
IHeftTaskStartHookOptions,
IHeftTaskFinishHookOptions,
IHeftPhaseStartHookOptions,
IHeftPhaseFinishHookOptions
} from './pluginFramework/HeftLifecycleSession';

export type {
Expand Down Expand Up @@ -79,3 +83,9 @@ export type {
CommandLineStringListParameter,
CommandLineStringParameter
} from '@rushstack/ts-command-line';

export type { IHeftTaskOperationMetadata } from './cli/HeftActionRunner';
export type { IHeftPhaseOperationMetadata } from './cli/HeftActionRunner';

export type { IHeftTask } from './pluginFramework/HeftTask';
export type { IHeftPhase } from './pluginFramework/HeftPhase';
12 changes: 10 additions & 2 deletions apps/heft/src/pluginFramework/HeftLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
type IHeftLifecycleHooks,
type IHeftLifecycleToolStartHookOptions,
type IHeftLifecycleToolFinishHookOptions,
type IHeftLifecycleSession
type IHeftLifecycleSession,
type IHeftTaskStartHookOptions,
type IHeftTaskFinishHookOptions,
type IHeftPhaseStartHookOptions,
type IHeftPhaseFinishHookOptions
} from './HeftLifecycleSession';
import type { ScopedLogger } from './logging/ScopedLogger';

Expand Down Expand Up @@ -67,7 +71,11 @@ export class HeftLifecycle extends HeftPluginHost {
clean: new AsyncParallelHook<IHeftLifecycleCleanHookOptions>(),
toolStart: new AsyncParallelHook<IHeftLifecycleToolStartHookOptions>(),
toolFinish: new AsyncParallelHook<IHeftLifecycleToolFinishHookOptions>(),
recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook
recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook,
taskStart: new AsyncParallelHook<IHeftTaskStartHookOptions>(['task']),
taskFinish: new AsyncParallelHook<IHeftTaskFinishHookOptions>(['task']),
phaseStart: new AsyncParallelHook<IHeftPhaseStartHookOptions>(['phase']),
phaseFinish: new AsyncParallelHook<IHeftPhaseFinishHookOptions>(['phase'])
};
}

Expand Down
62 changes: 62 additions & 0 deletions apps/heft/src/pluginFramework/HeftLifecycleSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type { IHeftParameters } from './HeftParameterManager';
import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin';
import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition';
import type { HeftPluginHost } from './HeftPluginHost';
import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph';
import type { IHeftPhaseOperationMetadata, IHeftTaskOperationMetadata } from '../cli/HeftActionRunner';

/**
* The lifecycle session is responsible for providing session-specific information to Heft lifecycle
Expand Down Expand Up @@ -67,6 +69,34 @@ export interface IHeftLifecycleSession {
): void;
}

/**
* @public
*/
export interface IHeftTaskStartHookOptions {
operation: Operation<IHeftTaskOperationMetadata>;
}

/**
* @public
*/
export interface IHeftTaskFinishHookOptions {
operation: Operation<IHeftTaskOperationMetadata>;
}

/**
* @public
*/
export interface IHeftPhaseStartHookOptions {
operation: OperationGroupRecord<IHeftPhaseOperationMetadata>;
}

/**
* @public
*/
export interface IHeftPhaseFinishHookOptions {
operation: OperationGroupRecord<IHeftPhaseOperationMetadata>;
}

/**
* Hooks that are available to the lifecycle plugin.
*
Expand Down Expand Up @@ -111,6 +141,38 @@ export interface IHeftLifecycleHooks {
* @public
*/
recordMetrics: AsyncParallelHook<IHeftRecordMetricsHookOptions>;

/**
* The `taskStart` hook is called at the beginning of a task. It is called before the task has begun
* to execute. To use it, call `taskStart.tapPromise(<pluginName>, <callback>)`.
*
* @public
*/
taskStart: AsyncParallelHook<IHeftTaskStartHookOptions>;

/**
* The `taskFinish` hook is called at the end of a task. It is called after the task has completed
* execution. To use it, call `taskFinish.tapPromise(<pluginName>, <callback>)`.
*
* @public
*/
taskFinish: AsyncParallelHook<IHeftTaskFinishHookOptions>;

/**
* The `phaseStart` hook is called at the beginning of a phase. It is called before the phase has
* begun to execute. To use it, call `phaseStart.tapPromise(<pluginName>, <callback>)`.
*
* @public
*/
phaseStart: AsyncParallelHook<IHeftPhaseStartHookOptions>;

/**
* The `phaseFinish` hook is called at the end of a phase. It is called after the phase has completed
* execution. To use it, call `phaseFinish.tapPromise(<pluginName>, <callback>)`.
*
* @public
*/
phaseFinish: AsyncParallelHook<IHeftPhaseFinishHookOptions>;
}

/**
Expand Down
Loading