@rushstack/heft
Version:
Build all your JavaScript projects the same way: A way that works.
171 lines • 8.41 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { createHash } from 'node:crypto';
import { glob } from 'fast-glob';
import { OperationStatus } from '@rushstack/operation-graph';
import { AlreadyReportedError, InternalError } from '@rushstack/node-core-library';
import { copyFilesAsync, asAbsoluteCopyOperation, asRelativeCopyOperation } from '../../plugins/CopyFilesPlugin';
import { deleteFilesAsync } from '../../plugins/DeleteFilesPlugin';
import { watchGlobAsync } from '../../plugins/FileGlobSpecifier';
import { WatchFileSystemAdapter } from '../../utilities/WatchFileSystemAdapter';
/**
* Log out a start message, run a provided function, and log out an end message
*/
export async function runAndMeasureAsync(fn, startMessageFn, endMessageFn, logFn) {
logFn(startMessageFn());
const startTime = performance.now();
try {
return await fn();
}
finally {
const endTime = performance.now();
logFn(`${endMessageFn()} (${endTime - startTime}ms)`);
}
}
export class TaskOperationRunner {
get name() {
const { taskName, parentPhase } = this._options.task;
return `Task ${JSON.stringify(taskName)} of phase ${JSON.stringify(parentPhase.phaseName)}`;
}
constructor(options) {
this._fileOperations = undefined;
this._watchFileSystemAdapter = undefined;
this.silent = false;
this._options = options;
}
async executeAsync(context) {
const { internalHeftSession, task } = this._options;
const { parentPhase } = task;
const phaseSession = internalHeftSession.getSessionForPhase(parentPhase);
const taskSession = phaseSession.getSessionForTask(task);
return await this._executeTaskAsync(context, taskSession);
}
async _executeTaskAsync(context, taskSession) {
const { abortSignal, requestRun } = context;
const { hooks, logger } = taskSession;
// Need to clear any errors or warnings from the previous invocation, particularly
// if this is an immediate rerun
logger.resetErrorsAndWarnings();
const rootFolderPath = this._options.internalHeftSession.heftConfiguration.buildFolderPath;
const isWatchMode = taskSession.parameters.watch && !!requestRun;
const { terminal } = logger;
// Exit the task early if cancellation is requested
if (abortSignal.aborted) {
return OperationStatus.Aborted;
}
if (!this._fileOperations && hooks.registerFileOperations.isUsed()) {
const fileOperations = await hooks.registerFileOperations.promise({
copyOperations: new Set(),
deleteOperations: new Set()
});
let copyConfigHash;
const { copyOperations } = fileOperations;
if (copyOperations.size > 0) {
// Do this here so that we only need to do it once for each Heft invocation
const hasher = createHash('sha256');
const absolutePathCopyOperations = new Set();
for (const copyOperation of fileOperations.copyOperations) {
// The paths in the `fileOperations` object may be either absolute or relative
// For execution we need absolute paths.
const absoluteOperation = asAbsoluteCopyOperation(rootFolderPath, copyOperation);
absolutePathCopyOperations.add(absoluteOperation);
// For portability of the hash we need relative paths.
const portableCopyOperation = asRelativeCopyOperation(rootFolderPath, absoluteOperation);
hasher.update(JSON.stringify(portableCopyOperation));
}
fileOperations.copyOperations = absolutePathCopyOperations;
copyConfigHash = hasher.digest('base64');
}
this._fileOperations = fileOperations;
this._copyConfigHash = copyConfigHash;
}
const shouldRunIncremental = isWatchMode && hooks.runIncremental.isUsed();
let watchFileSystemAdapter;
const getWatchFileSystemAdapter = () => {
if (!watchFileSystemAdapter) {
watchFileSystemAdapter = this._watchFileSystemAdapter || (this._watchFileSystemAdapter = new WatchFileSystemAdapter());
watchFileSystemAdapter.setBaseline();
}
return watchFileSystemAdapter;
};
const shouldRun = hooks.run.isUsed() || shouldRunIncremental;
if (!shouldRun && !this._fileOperations) {
terminal.writeVerboseLine('Task execution skipped, no implementation provided');
return OperationStatus.NoOp;
}
const runResult = shouldRun
? await runAndMeasureAsync(async () => {
// Create the options and provide a utility method to obtain paths to copy
const runHookOptions = {
abortSignal,
globAsync: glob
};
// Run the plugin run hook
try {
if (shouldRunIncremental) {
const runIncrementalHookOptions = {
...runHookOptions,
watchGlobAsync: (pattern, options = {}) => {
return watchGlobAsync(pattern, {
...options,
fs: getWatchFileSystemAdapter()
});
},
get watchFs() {
return getWatchFileSystemAdapter();
},
requestRun: requestRun
};
await hooks.runIncremental.promise(runIncrementalHookOptions);
}
else {
await hooks.run.promise(runHookOptions);
}
}
catch (e) {
// Log out using the task logger, and return an error status
if (!(e instanceof AlreadyReportedError)) {
logger.emitError(e);
}
return OperationStatus.Failure;
}
if (abortSignal.aborted) {
return OperationStatus.Aborted;
}
return OperationStatus.Success;
}, () => `Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`, () => {
const finishedWord = abortSignal.aborted ? 'Aborted' : 'Finished';
return `${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution`;
}, terminal.writeVerboseLine.bind(terminal))
: // This branch only occurs if only file operations are defined.
OperationStatus.Success;
if (this._fileOperations) {
const { copyOperations, deleteOperations } = this._fileOperations;
const copyConfigHash = this._copyConfigHash;
await Promise.all([
copyConfigHash
? copyFilesAsync(copyOperations, logger.terminal, `${taskSession.tempFolderPath}/file-copy.json`, copyConfigHash, isWatchMode ? getWatchFileSystemAdapter() : undefined)
: Promise.resolve(),
deleteOperations.size > 0
? deleteFilesAsync(rootFolderPath, deleteOperations, logger.terminal)
: Promise.resolve()
]);
}
if (watchFileSystemAdapter) {
if (!requestRun) {
throw new InternalError(`watchFileSystemAdapter was initialized but requestRun is not defined!`);
}
watchFileSystemAdapter.watch(requestRun);
}
// Even if the entire process has completed, we should mark the operation as cancelled if
// cancellation has been requested.
if (abortSignal.aborted) {
return OperationStatus.Aborted;
}
if (logger.hasErrors) {
return OperationStatus.Failure;
}
return runResult;
}
}
//# sourceMappingURL=TaskOperationRunner.js.map