diff --git a/bin/common/parse-cli-args.js b/bin/common/parse-cli-args.js index d3c1fc0..34f9fec 100644 --- a/bin/common/parse-cli-args.js +++ b/bin/common/parse-cli-args.js @@ -79,6 +79,7 @@ class ArgumentSet { constructor(initialValues, options) { this.continueOnError = false this.groups = [] + this.maxParallel = 0 this.printLabel = false this.printName = false this.race = false @@ -123,6 +124,11 @@ function parseCLIArgsCore(set, args) { // eslint-disable-line complexity set.rest = args.slice(1 + i) break LOOP + case "--color": + case "--no-color": + // do nothing. + break + case "-c": case "--continue-on-error": set.continueOnError = true @@ -147,9 +153,11 @@ function parseCLIArgsCore(set, args) { // eslint-disable-line complexity set.silent = true break - case "--color": - case "--no-color": - // do nothing. + case "--max-parallel": + set.maxParallel = parseInt(args[++i], 10) + if (!Number.isFinite(set.maxParallel) || set.maxParallel <= 0) { + throw new Error(`Invalid Option: --max-parallel ${args[i]}`) + } break case "-s": @@ -205,9 +213,11 @@ function parseCLIArgsCore(set, args) { // eslint-disable-line complexity } if (!set.parallel && set.race) { - throw new Error(`Invalid Option: ${ - args.indexOf("--race") !== -1 ? "race" : "r" - } (without parallel)`) + const race = args.indexOf("--race") !== -1 ? "race" : "r" + throw new Error(`Invalid Option: ${race} (without parallel)`) + } + if (!set.parallel && set.maxParallel !== 0) { + throw new Error("Invalid Option: --max-parallel (without parallel)") } return set diff --git a/bin/npm-run-all/help.js b/bin/npm-run-all/help.js index 811e97a..742999b 100644 --- a/bin/npm-run-all/help.js +++ b/bin/npm-run-all/help.js @@ -31,6 +31,8 @@ Options: other/subsequent tasks even if a task threw an error. 'npm-run-all' itself will exit with non-zero code if one or more tasks threw error(s) + --max-parallel - Set the maximum number of parallelism. Default is + unlimited. -l, --print-label - - - - Set the flag to print the task name as a prefix on each line of output. Tools in tasks may stop coloring their output if this option was given. diff --git a/bin/npm-run-all/main.js b/bin/npm-run-all/main.js index 44f60b4..3db9c5f 100644 --- a/bin/npm-run-all/main.js +++ b/bin/npm-run-all/main.js @@ -42,6 +42,7 @@ module.exports = function npmRunAll(args, stdout, stderr) { stderr, stdin, parallel: group.parallel, + maxParallel: argv.maxParallel, continueOnError: argv.continueOnError, printLabel: argv.printLabel, printName: argv.printName, diff --git a/bin/run-p/help.js b/bin/run-p/help.js index ffdcd3a..023766f 100644 --- a/bin/run-p/help.js +++ b/bin/run-p/help.js @@ -31,6 +31,8 @@ Options: even if a task threw an error. 'run-p' itself will exit with non-zero code if one or more tasks threw error(s). + --max-parallel - Set the maximum number of parallelism. Default is + unlimited. -l, --print-label - - - - Set the flag to print the task name as a prefix on each line of output. Tools in tasks may stop coloring their output if this option was given. diff --git a/bin/run-p/main.js b/bin/run-p/main.js index e6bef6b..71b6544 100644 --- a/bin/run-p/main.js +++ b/bin/run-p/main.js @@ -42,6 +42,7 @@ module.exports = function npmRunAll(args, stdout, stderr) { stderr, stdin, parallel: group.parallel, + maxParallel: argv.maxParallel, continueOnError: argv.continueOnError, printLabel: argv.printLabel, printName: argv.printName, diff --git a/docs/node-api.md b/docs/node-api.md index 59f945d..a9de490 100644 --- a/docs/node-api.md +++ b/docs/node-api.md @@ -45,6 +45,9 @@ Run npm-scripts. - **options.parallel** `boolean` -- The flag to run scripts in parallel. Default is `false`. + - **options.maxParallel** `number` -- + The maximum number of parallelism. + Default is `Number.POSITIVE_INFINITY`. - **options.packageConfig** `object|null` -- The map-like object to overwrite package configs. Keys are package names. diff --git a/docs/npm-run-all.md b/docs/npm-run-all.md index 820780c..4118eec 100644 --- a/docs/npm-run-all.md +++ b/docs/npm-run-all.md @@ -36,6 +36,8 @@ Options: other/subsequent tasks even if a task threw an error. 'npm-run-all' itself will exit with non-zero code if one or more tasks threw error(s) + --max-parallel - Set the maximum number of parallelism. Default is + unlimited. -l, --print-label - - - - Set the flag to print the task name as a prefix on each line of output. Tools in tasks may stop coloring their output if this option was given. @@ -109,7 +111,7 @@ npm-run-all clean lint --parallel watch:html watch:js ``` 1. First, this runs `clean` and `lint` sequentially / serially. -2. Next, runs `watch:html` and `watch:js` in parallell. +2. Next, runs `watch:html` and `watch:js` in parallel. ``` npm-run-all a b --parallel c d --sequential e f --parallel g h i @@ -121,9 +123,9 @@ npm-run-all a b --parallel c d --serial e f --parallel g h i ``` 1. First, runs `a` and `b` sequentially / serially. -2. Second, runs `c` and `d` in parallell. +2. Second, runs `c` and `d` in parallel. 3. Third, runs `e` and `f` sequentially / serially. -4. Lastly, runs `g`, `h`, and `i` in parallell. +4. Lastly, runs `g`, `h`, and `i` in parallel. ### Glob-like pattern matching for script names diff --git a/docs/run-p.md b/docs/run-p.md index 4cacf34..d74dbe7 100644 --- a/docs/run-p.md +++ b/docs/run-p.md @@ -35,6 +35,8 @@ Options: even if a task threw an error. 'run-p' itself will exit with non-zero code if one or more tasks threw error(s). + --max-parallel - Set the maximum number of parallelism. Default is + unlimited. -l, --print-label - - - - Set the flag to print the task name as a prefix on each line of output. Tools in tasks may stop coloring their output if this option was given. diff --git a/lib/index.js b/lib/index.js index bb87479..9b7e1df 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,8 +13,7 @@ const shellQuote = require("shell-quote") const matchTasks = require("./match-tasks") const readPackageJson = require("./read-package-json") -const runTasksInParallel = require("./run-tasks-in-parallel") -const runTasksInSequencial = require("./run-tasks-in-sequencial") +const runTasks = require("./run-tasks") //------------------------------------------------------------------------------ // Helpers @@ -204,7 +203,6 @@ function maxLength(length, name) { * A promise object which becomes fullfilled when all npm-scripts are completed. */ module.exports = function npmRunAll(patternOrPatterns, options) { - const parallel = Boolean(options && options.parallel) const stdin = (options && options.stdin) || null const stdout = (options && options.stdout) || null const stderr = (options && options.stderr) || null @@ -212,20 +210,27 @@ module.exports = function npmRunAll(patternOrPatterns, options) { const config = (options && options.config) || null const packageConfig = (options && options.packageConfig) || null const args = (options && options.arguments) || [] + const parallel = Boolean(options && options.parallel) const silent = Boolean(options && options.silent) const continueOnError = Boolean(options && options.continueOnError) const printLabel = Boolean(options && options.printLabel) const printName = Boolean(options && options.printName) const race = Boolean(options && options.race) + const maxParallel = parallel ? ((options && options.maxParallel) || 0) : 1 try { const patterns = parsePatterns(patternOrPatterns, args) if (patterns.length === 0) { return Promise.resolve(null) } - if (taskList != null && Array.isArray(taskList) === false) { throw new Error("Invalid options.taskList") } + if (typeof maxParallel !== "number" || !(maxParallel >= 0)) { + throw new Error("Invalid options.maxParallel") + } + if (!parallel && race) { + throw new Error("Invalid options.race") + } const prefixOptions = [].concat( silent ? ["--silent"] : [], @@ -243,7 +248,6 @@ module.exports = function npmRunAll(patternOrPatterns, options) { .then(x => { const tasks = matchTasks(x.taskList, patterns) const labelWidth = tasks.reduce(maxLength, 0) - const runTasks = parallel ? runTasksInParallel : runTasksInSequencial return runTasks(tasks, { stdin, @@ -260,6 +264,7 @@ module.exports = function npmRunAll(patternOrPatterns, options) { printName, packageInfo: x.packageInfo, race, + maxParallel, }) }) } diff --git a/lib/run-tasks-in-parallel.js b/lib/run-tasks-in-parallel.js deleted file mode 100644 index 40d37e7..0000000 --- a/lib/run-tasks-in-parallel.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @module run-tasks-in-parallel - * @author Toru Nagashima - * @copyright 2015 Toru Nagashima. All rights reserved. - * See LICENSE file in root directory for full license. - */ -"use strict" - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const NpmRunAllError = require("./npm-run-all-error") -const runTask = require("./run-task") - -//------------------------------------------------------------------------------ -// 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 {object} options - An option object. - * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed. - * @private - */ -module.exports = function runTasksInParallel(tasks, options) { - const taskPromises = tasks.map(task => runTask(task, options)) - const results = tasks.map(task => ({name: task, code: undefined})) - let aborted = false - - /** - * Aborts all tasks. - * @returns {void} - */ - function abortTasks() { - aborted = true - taskPromises.forEach(t => t.abort()) - } - - // When one of tasks exited with non-zero, abort all tasks. - // And wait for all tasks exit. - let errorResult = null - const parallelPromise = Promise.all(taskPromises.map((promise, index) => - promise.then(result => { - if (aborted) { - return - } - - // Save the result. - results[index].code = result.code - - // Aborts all tasks if it's an error. - if (errorResult == null && result.code) { - errorResult = errorResult || result - if (!options.continueOnError) { - abortTasks() - } - } - - // Aborts all tasks if options.race is true. - if (options.race && !result.code) { - abortTasks() - } - }) - )) - parallelPromise.catch(() => { - if (!aborted && !options.continueOnError) { - abortTasks() - } - }) - - // Make fail if there are tasks that exited non-zero. - return parallelPromise.then(() => { - if (errorResult != null) { - throw new NpmRunAllError(errorResult, results) - } - return results - }) -} diff --git a/lib/run-tasks-in-sequencial.js b/lib/run-tasks-in-sequencial.js deleted file mode 100644 index 52fa7b8..0000000 --- a/lib/run-tasks-in-sequencial.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @module run-tasks-in-sequencial - * @author Toru Nagashima - * @copyright 2015 Toru Nagashima. All rights reserved. - * See LICENSE file in root directory for full license. - */ -"use strict" - -//------------------------------------------------------------------------------ -// Requirements -//------------------------------------------------------------------------------ - -const NpmRunAllError = require("./npm-run-all-error") -const runTask = require("./run-task") - -//------------------------------------------------------------------------------ -// Public Interface -//------------------------------------------------------------------------------ - -/** - * Run npm-scripts of given names in sequencial. - * - * If a npm-script exited with a non-zero code, this aborts subsequent npm-scripts. - * - * @param {string} tasks - A list of npm-script name to run in sequencial. - * @param {object} options - An option object. - * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed. - * @private - */ -module.exports = function runTasksInSequencial(tasks, options) { - const results = tasks.map(task => ({name: task, code: undefined})) - let errorResult = null - let index = 0 - - /** - * Saves a given result and checks the result code. - * - * @param {{task: string, code: number}} result - The result item. - * @returns {void} - */ - function postprocess(result) { - if (result == null) { - return - } - results[index++].code = result.code - - if (result.code) { - if (options.continueOnError) { - errorResult = errorResult || result - } - else { - throw new NpmRunAllError(result, results) - } - } - } - - return tasks - .reduce( - (prev, task) => ( - prev.then(result => (( - postprocess(result), - runTask(task, options) - ))) - ), - Promise.resolve(null) - ) - .then(result => { - postprocess(result) - - if (errorResult != null) { - throw new NpmRunAllError(errorResult, results) - } - return results - }) -} diff --git a/lib/run-tasks.js b/lib/run-tasks.js new file mode 100644 index 0000000..72a93f3 --- /dev/null +++ b/lib/run-tasks.js @@ -0,0 +1,159 @@ +/** + * @module run-tasks-in-parallel + * @author Toru Nagashima + * @copyright 2015 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ +"use strict" + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const NpmRunAllError = require("./npm-run-all-error") +const runTask = require("./run-task") + +//------------------------------------------------------------------------------ +// 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) + } +} + +//------------------------------------------------------------------------------ +// 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 {object} options - An option object. + * @returns {Promise} A promise object which becomes fullfilled when all npm-scripts are completed. + * @private + */ +module.exports = function runTasks(tasks, options) { + return new Promise((resolve, reject) => { + if (tasks.length === 0) { + resolve([]) + return + } + + const results = tasks.map(task => ({name: task, code: undefined})) + const queue = tasks.map((task, index) => ({name: task, index})) + const promises = [] + 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 { + promises.forEach(p => 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 task = queue.shift() + const promise = runTask(task.name, options) + + promises.push(promise) + promise.then( + (result) => { + remove(promises, promise) + if (aborted) { + return + } + + // Save the result. + results[task.index].code = result.code + + // Aborts all tasks if it's an error. + if (result.code) { + error = new NpmRunAllError(result, 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() + } + }) +} diff --git a/test/lib/util.js b/test/lib/util.js index a7caf98..8794600 100644 --- a/test/lib/util.js +++ b/test/lib/util.js @@ -52,11 +52,8 @@ function spawn(filePath, args, stdout, stderr) { child.stderr.pipe(error) } child.on("close", (exitCode) => { - if (error.value) { - console.log(error.value) - } if (exitCode) { - reject(new Error("Exited with non-zero code.")) + reject(new Error(error.value || "Exited with non-zero code.")) } else { resolve() diff --git a/test/parallel.js b/test/parallel.js index ef8c837..18c7848 100644 --- a/test/parallel.js +++ b/test/parallel.js @@ -310,4 +310,46 @@ describe("[parallel]", () => { }) ) }) + + describe("should run tasks in parallel-2 when was given --max-parallel 2 option:", () => { + it("Node API", () => + nodeApi(["test-task:append a", "test-task:append b", "test-task:append c"], {parallel: true, maxParallel: 2}) + .then(results => { + assert(results.length === 3) + assert(results[0].name === "test-task:append a") + assert(results[0].code === 0) + assert(results[1].name === "test-task:append b") + assert(results[1].code === 0) + assert(results[2].name === "test-task:append c") + assert(results[2].code === 0) + assert( + result() === "ababcc" || + result() === "babacc" || + result() === "abbacc" || + result() === "baabcc") + }) + ) + + it("npm-run-all command", () => + runAll(["--parallel", "test-task:append a", "test-task:append b", "test-task:append c", "--max-parallel", "2"]) + .then(() => { + assert( + result() === "ababcc" || + result() === "babacc" || + result() === "abbacc" || + result() === "baabcc") + }) + ) + + it("run-p command", () => + runPar(["test-task:append a", "test-task:append b", "test-task:append c", "--max-parallel", "2"]) + .then(() => { + assert( + result() === "ababcc" || + result() === "babacc" || + result() === "abbacc" || + result() === "baabcc") + }) + ) + }) })