UNPKG

@rushstack/heft

Version:

Build all your JavaScript projects the same way: A way that works.

171 lines 8.41 kB
// 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