UNPKG

npm-run-all2

Version:

A CLI tool to run multiple npm-scripts in parallel or sequential. (Maintenance fork)

242 lines (216 loc) 6.17 kB
/** * @module run-tasks-in-parallel * @author Toru Nagashima * @copyright 2015 Toru Nagashima. All rights reserved. * @copyright 2026 Bret Comnes. All rights reserved. * See LICENSE file in root directory for full license. * * @import { Writable } from 'node:stream' * @import { AbortableRunTaskPromise, RunTaskOptions } from './run-task.js' * @import { NpmRunAllResult } from './index.js' */ /** * @typedef {RunTaskOptions & { * continueOnError: boolean, * race: boolean, * maxParallel: number, * aggregateOutput: boolean, * stdout: Writable | null * }} RunTasksOptions */ // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ import MemoryStream from 'memorystream' import NpmRunAllError from './npm-run-all-error.js' import runTask from './run-task.js' // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ /** * Remove the given value from the array. * @template T * @param {T[]} array - The array to remove. * @param {T} x - The item to be removed. * @returns {void} */ function remove (array, x) { const index = array.indexOf(x) if (index !== -1) { array.splice(index, 1) } } /** @type {Record<string, number>} */ const signals = { SIGABRT: 6, SIGALRM: 14, SIGBUS: 10, SIGCHLD: 20, SIGCONT: 19, SIGFPE: 8, SIGHUP: 1, SIGILL: 4, SIGINT: 2, SIGKILL: 9, SIGPIPE: 13, SIGQUIT: 3, SIGSEGV: 11, SIGSTOP: 17, SIGTERM: 15, SIGTRAP: 5, SIGTSTP: 18, SIGTTIN: 21, SIGTTOU: 22, SIGUSR1: 30, SIGUSR2: 31, } /** * Converts a signal name to a number. * @param {string | null} signal - the signal name to convert into a number * @returns {number} - the return code for the signal */ function convert (signal) { return signal ? signals[signal] || 0 : 0 } // ------------------------------------------------------------------------------ // Public Interface // ------------------------------------------------------------------------------ /** * Run npm-scripts of given names in parallel. * * If a npm-script exited with a non-zero code, this aborts other all npm-scripts. * * @param {string[]} tasks - A list of npm-script name to run in parallel. * @param {RunTasksOptions} options - An option object. * @returns {Promise<NpmRunAllResult[]>} A promise object which becomes fulfilled when all npm-scripts are completed. * @private */ export default function runTasks (tasks, options) { return new Promise((resolve, reject) => { if (tasks.length === 0) { resolve([]) return } /** @type {NpmRunAllResult[]} */ const results = tasks.map(task => ({ name: task, code: undefined })) const queue = tasks.map((task, index) => ({ name: task, index })) /** @type {AbortableRunTaskPromise[]} */ const promises = [] /** @type {Error | null} */ let error = null let aborted = false /** * Done. * @returns {void} */ function done () { if (error == null) { resolve(results) } else { reject(error) } } /** * Aborts all tasks. * @returns {void} */ function abort () { if (aborted) { return } aborted = true if (promises.length === 0) { done() } else { for (const p of promises) { p.abort() } Promise.all(promises).then(done, reject) } } /** * Runs a next task. * @returns {void} */ function next () { if (aborted) { return } if (queue.length === 0) { if (promises.length === 0) { done() } return } const originalOutputStream = options.stdout const optionsClone = { ...options } const writer = new MemoryStream(undefined, { readable: false, }) if (options.aggregateOutput) { optionsClone.stdout = writer } const task = queue.shift() if (!task) { return } const promise = runTask(task.name, optionsClone) promises.push(promise) promise.then( (result) => { remove(promises, promise) if (aborted) { return } if (options.aggregateOutput && originalOutputStream != null) { originalOutputStream.write(writer.toString()) } // Check if the task failed as a result of a signal, and // amend the exit code as a result. if (result.code === null && result.signal !== null) { // An exit caused by a signal must return a status code // of 128 plus the value of the signal code. // Ref: https://nodejs.org/api/process.html#process_exit_codes result.code = 128 + convert(result.signal) } // Save the result. const resultItem = results[task.index] if (resultItem) { resultItem.code = result.code ?? undefined } // Aborts all tasks if it's an error. if (result.code) { error = new NpmRunAllError({ name: result.task, task: result.task, code: result.code }, results) if (!options.continueOnError) { abort() return } } // Aborts all tasks if options.race is true. if (options.race && !result.code) { abort() return } // Call the next task. next() }, (thisError) => { remove(promises, promise) if (!options.continueOnError || options.race) { error = thisError abort() return } next() } ) } const max = options.maxParallel const end = (typeof max === 'number' && max > 0) ? Math.min(tasks.length, max) : tasks.length for (let i = 0; i < end; ++i) { next() } }) }