diff --git a/package.json b/package.json index cd466ee7..13704180 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "concurrently", "version": "3.5.0", "description": "Run commands concurrently", - "main": "src/main.js", + "main": "src/api.js", + "typings": "typings.d.ts", "bin": { "concurrent": "./src/main.js", "concurrently": "./src/main.js" diff --git a/src/api.js b/src/api.js new file mode 100644 index 00000000..d0a87b4b --- /dev/null +++ b/src/api.js @@ -0,0 +1,28 @@ +var _ = require('lodash'); +var lib = require('./lib.js'); + +module.exports = function(commands, options) { + const prefixColors = []; + const names = []; + const executables = []; + _.each(commands, cmd => { + if (typeof cmd === 'string') { + prefixColors.push(''); + names.push(''); + executables.push(cmd); + } else { + const cmdColors = _.filter([cmd.prefixModifier, cmd.prefixTextColor, cmd.prefixBackColor], o => o); + prefixColors.push(cmdColors.length > 0 ? _.join(cmdColors, '.') : ''); + names.push(cmd.name || ''); + executables.push(cmd.command); + } + }); + + _.assign(lib.config, options); + + return new Promise(resolve => { + lib.run(executables, names, prefixColors, childExitCodes => { + resolve(childExitCodes); + }); + }); +} \ No newline at end of file diff --git a/src/lib.js b/src/lib.js new file mode 100644 index 00000000..39b8e6bc --- /dev/null +++ b/src/lib.js @@ -0,0 +1,375 @@ +var Rx = require('rx'); +var path = require('path'); +var formatDate = require('date-fns/format'); +var _ = require('lodash'); +var treeKill = require('tree-kill'); +var chalk = require('chalk'); +var spawn = require('spawn-command'); +var supportsColor = require('supports-color'); +var IS_WINDOWS = /^win/.test(process.platform); + +var config = { + // Kill other processes if one dies + killOthers: false, + + // Kill other processes if one exits with non zero status code + killOthersOnFail: false, + + // Return success or failure of the 'first' child to terminate, the 'last' child, + // or succeed only if 'all' children succeed + success: 'all', + + // Prefix logging with pid + // Possible values: 'pid', 'none', 'time', 'command', 'index', 'name' + prefix: '', + + // List of custom names to be used in prefix template + names: '', + + // What to split the list of custom names on + nameSeparator: ',', + + // Comma-separated list of chalk color paths to use on prefixes. + prefixColors: 'gray.dim', + + // moment/date-fns format + timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS', + + // How many characters to display from start of command in prefix if + // command is defined. Note that also '..' will be added in the middle + prefixLength: 10, + + // By default, color output + color: true, + + // If true, the output will only be raw output of processes, nothing more + raw: false, + + // If true, the process restart when it exited with status code non-zero + allowRestart: false, + + // By default, restart instantly + restartAfter: 0, + + // By default, restart once + restartTries: 1 +}; + +function stripCmdQuotes(cmd) { + // Removes the quotes surrounding a command. + if (cmd[0] === '"' || cmd[0] === '\'') { + return cmd.substr(1, cmd.length - 2); + } else { + return cmd; + } +} + +function run(commands, names, prefixColors, exitCallback) { + if (!config.prefix) { + config.prefix = names ? 'name' : 'index'; + } + + var childrenInfo = {}; + var lastPrefixColor = _.get(chalk, chalk.gray.dim); + var children = _.map(commands, function(cmd, index) { + // Remove quotes. + cmd = stripCmdQuotes(cmd); + + var spawnOpts = config.raw ? { stdio: 'inherit' } : {}; + if (IS_WINDOWS) { + spawnOpts.detached = false; + } + if (supportsColor) { + spawnOpts.env = Object.assign({ FORCE_COLOR: supportsColor.level }, process.env) + } + + var child = spawnChild(cmd, spawnOpts); + + if (index < prefixColors.length) { + var prefixColorPath = prefixColors[index]; + lastPrefixColor = _.get(chalk, prefixColorPath, chalk.gray.dim); + } + + var name = index < names.length ? names[index] : ''; + childrenInfo[child.pid] = { + command: cmd, + index: index, + name: name, + options: spawnOpts, + restartTries: config.restartTries, + prefixColor: lastPrefixColor + }; + return child; + }); + + var streams = toStreams(children); + + handleChildEvents(streams, children, childrenInfo, exitCallback); + + ['SIGINT', 'SIGTERM'].forEach(function(signal) { + process.on(signal, function() { + children.forEach(function(child) { + treeKill(child.pid, signal); + }); + }); + }); +} + +function runCommands(commands) { + var prefixColors = config.prefixColors.split(','); + var names = config.names.split(config.nameSeparator); + if (!config.prefix) { + config.prefix = config.names ? 'name' : 'index'; + } + run(commands, names, prefixColors, exit); +} + +function spawnChild(cmd, options) { + var child; + try { + child = spawn(cmd, options); + } catch (e) { + logError('', chalk.gray.dim, 'Error occured when executing command: ' + cmd); + logError('', chalk.gray.dim, e.stack); + process.exit(1); + } + return child; +} + +function toStreams(children) { + // Transform all process events to rx streams + return _.map(children, function(child) { + var childStreams = { + error: Rx.Node.fromEvent(child, 'error'), + close: Rx.Node.fromEvent(child, 'close') + }; + if (!config.raw) { + childStreams.stdout = Rx.Node.fromReadableStream(child.stdout); + childStreams.stderr = Rx.Node.fromReadableStream(child.stderr); + } + + return _.reduce(childStreams, function(memo, stream, key) { + memo[key] = stream.map(function(data) { + return { child: child, data: data }; + }); + + return memo; + }, {}); + }); +} + +function handleChildEvents(streams, children, childrenInfo, exitCallback) { + handleClose(streams, children, childrenInfo, exitCallback); + handleError(streams, childrenInfo); + if (!config.raw) { + handleOutput(streams, childrenInfo, 'stdout'); + handleOutput(streams, childrenInfo, 'stderr'); + } +} + +function handleOutput(streams, childrenInfo, source) { + var sourceStreams = _.map(streams, source); + var combinedSourceStream = Rx.Observable.merge.apply(this, sourceStreams); + + combinedSourceStream.subscribe(function(event) { + var prefix = getPrefix(childrenInfo, event.child); + var prefixColor = childrenInfo[event.child.pid].prefixColor; + log(prefix, prefixColor, event.data.toString()); + }); +} + +function handleClose(streams, children, childrenInfo, exitCallback) { + var allChildren = _.clone(children); + var aliveChildren = _.clone(children); + var exitCodes = []; + var closeStreams = _.map(streams, 'close'); + var closeStream = Rx.Observable.merge.apply(this, closeStreams); + var othersKilled = false + + // TODO: Is it possible that amount of close events !== count of spawned? + closeStream.subscribe(function(event) { + var exitCode = event.data; + var nonSuccess = exitCode !== 0; + exitCodes[allChildren.indexOf(event.child)] = exitCode; + + var prefix = getPrefix(childrenInfo, event.child); + var childInfo = childrenInfo[event.child.pid]; + var prefixColor = childInfo.prefixColor; + var command = childInfo.command; + logEvent(prefix, prefixColor, command + ' exited with code ' + exitCode); + + aliveChildren = _.filter(aliveChildren, function(child) { + return child.pid !== event.child.pid; + }); + + if (nonSuccess && config.allowRestart && childInfo.restartTries--) { + respawnChild(event, childrenInfo, exitCallback); + return; + } + + if (aliveChildren.length === 0) { + exitCallback(exitCodes); + } + if (!othersKilled) { + if (config.killOthers) { + killOtherProcesses(aliveChildren); + othersKilled = true; + } else if (config.killOthersOnFail && nonSuccess) { + killOtherProcesses(aliveChildren); + othersKilled = true; + } + } + }); +} + +function respawnChild(event, childrenInfo, exitCallback) { + setTimeout(function() { + var childInfo = childrenInfo[event.child.pid]; + var prefix = getPrefix(childrenInfo, event.child); + var prefixColor = childInfo.prefixColor; + logEvent(prefix, prefixColor, childInfo.command + ' restarted'); + var newChild = spawnChild(childInfo.command, childInfo.options); + + childrenInfo[newChild.pid] = childrenInfo[event.child.pid]; + delete childrenInfo[event.child.pid]; + + var children = [newChild]; + var streams = toStreams(children); + handleChildEvents(streams, children, childrenInfo, exitCallback); + }, config.restartAfter); +} + +function killOtherProcesses(processes) { + logEvent('--> ', chalk.gray.dim, 'Sending SIGTERM to other processes..'); + + // Send SIGTERM to alive children + _.each(processes, function(child) { + treeKill(child.pid, 'SIGTERM'); + }); +} + +function exit(childExitCodes) { + var success; + switch (config.success) { + case 'first': + success = _.first(childExitCodes) === 0; + break; + case 'last': + success = _.last(childExitCodes) === 0; + break; + default: + success = _.every(childExitCodes, function(code) { + return code === 0; + }); + } + process.exit(success ? 0 : 1); +} + +function handleError(streams, childrenInfo) { + // Output emitted errors from child process + var errorStreams = _.map(streams, 'error'); + var processErrorStream = Rx.Observable.merge.apply(this, errorStreams); + + processErrorStream.subscribe(function(event) { + var command = childrenInfo[event.child.pid].command; + logError('', chalk.gray.dim, 'Error occured when executing command: ' + command); + logError('', chalk.gray.dim, event.data.stack); + }); +} + +function colorText(text, color) { + if (!config.color) { + return text; + } else { + return color(text); + } +} + +function getPrefix(childrenInfo, child) { + var prefixes = getPrefixes(childrenInfo, child); + if (_.includes(_.keys(prefixes), config.prefix)) { + return '[' + prefixes[config.prefix] + '] '; + } + + return _.reduce(prefixes, function(memo, val, key) { + var re = new RegExp('{' + key + '}', 'g'); + return memo.replace(re, val); + }, config.prefix) + ' '; +} + +function getPrefixes(childrenInfo, child) { + var prefixes = {}; + + prefixes.none = ''; + prefixes.pid = child.pid; + prefixes.index = childrenInfo[child.pid].index; + prefixes.name = childrenInfo[child.pid].name; + prefixes.time = formatDate(Date.now(), config.timestampFormat); + + var command = childrenInfo[child.pid].command; + prefixes.command = shortenText(command, config.prefixLength); + return prefixes; +} + +function shortenText(text, length, cut) { + if (text.length <= length) { + return text; + } + cut = _.isString(cut) ? cut :  '..'; + + var endLength = Math.floor(length / 2); + var startLength = length - endLength; + + var first = text.substring(0, startLength); + var last = text.substring(text.length - endLength, text.length); + return first + cut + last; +} + +function log(prefix, prefixColor, text) { + logWithPrefix(prefix, prefixColor, text); +} + +function logEvent(prefix, prefixColor, text) { + if (config.raw) return; + + logWithPrefix(prefix, prefixColor, text, chalk.gray.dim); +} + +function logError(prefix, prefixColor, text) { + // This is for now same as log, there might be separate colors for stderr + // and stdout + logWithPrefix(prefix, prefixColor, text, chalk.red.bold); +} + +function logWithPrefix(prefix, prefixColor, text, color) { + var lastChar = text[text.length - 1]; + if (config.raw) { + if (lastChar !== '\n') { + text += '\n'; + } + + process.stdout.write(text); + return; + } + + if (lastChar === '\n') { + // Remove extra newline from the end to prevent extra newlines in input + text = text.slice(0, text.length - 1); + } + + var lines = text.split('\n'); + // Do not bgColor trailing space + var coloredPrefix = colorText(prefix.replace(/ $/, ''), prefixColor) + ' '; + var paddedLines = _.map(lines, function(line, i) { + var coloredLine = color ? colorText(line, color) : line; + return coloredPrefix + coloredLine; + }); + + console.log(paddedLines.join('\n')); +} + +module.exports = { + config, + run, + runCommands +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 4164eb4c..b154cd94 100755 --- a/src/main.js +++ b/src/main.js @@ -1,62 +1,11 @@ #!/usr/bin/env node -var Rx = require('rx'); var path = require('path'); -var formatDate = require('date-fns/format'); var program = require('commander'); var _ = require('lodash'); -var treeKill = require('tree-kill'); -var chalk = require('chalk'); -var spawn = require('spawn-command'); -var supportsColor = require('supports-color'); -var IS_WINDOWS = /^win/.test(process.platform); -var config = { - // Kill other processes if one dies - killOthers: false, - - // Kill other processes if one exits with non zero status code - killOthersOnFail: false, - - // Return success or failure of the 'first' child to terminate, the 'last' child, - // or succeed only if 'all' children succeed - success: 'all', - - // Prefix logging with pid - // Possible values: 'pid', 'none', 'time', 'command', 'index', 'name' - prefix: '', - - // List of custom names to be used in prefix template - names: '', - - // What to split the list of custom names on - nameSeparator: ',', - - // Comma-separated list of chalk color paths to use on prefixes. - prefixColors: 'gray.dim', - - // moment/date-fns format - timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS', - - // How many characters to display from start of command in prefix if - // command is defined. Note that also '..' will be added in the middle - prefixLength: 10, - - // By default, color output - color: true, - - // If true, the output will only be raw output of processes, nothing more - raw: false, - - // If true, the process restart when it exited with status code non-zero - allowRestart: false, - - // By default, restart instantly - restartAfter: 0, - - // By default, restart once - restartTries: 1 -}; +var lib = require('./lib.js'); +var config = lib.config; function main() { var firstBase = path.basename(process.argv[0]); @@ -66,10 +15,9 @@ function main() { } parseArgs(); - config = mergeDefaultsWithArgs(config); - applyDynamicDefaults(config) + config = _.merge(config, program); - run(program.args); + lib.runCommands(program.args); } function parseArgs() { @@ -193,316 +141,4 @@ function parseArgs() { program.parse(process.argv); } -function mergeDefaultsWithArgs(config) { - // This will pollute config object with other attributes from program too - return _.merge(config, program); -} - -function applyDynamicDefaults(config) { - if (!config.prefix) { - config.prefix = config.names ? 'name' : 'index'; - } -} - -function stripCmdQuotes(cmd) { - // Removes the quotes surrounding a command. - if (cmd[0] === '"' || cmd[0] === '\'') { - return cmd.substr(1, cmd.length - 2); - } else { - return cmd; - } -} - -function run(commands) { - var childrenInfo = {}; - var lastPrefixColor = _.get(chalk, chalk.gray.dim); - var prefixColors = config.prefixColors.split(','); - var names = config.names.split(config.nameSeparator); - var children = _.map(commands, function(cmd, index) { - // Remove quotes. - cmd = stripCmdQuotes(cmd); - - var spawnOpts = config.raw ? {stdio: 'inherit'} : {}; - if (IS_WINDOWS) { - spawnOpts.detached = false; - } - if (supportsColor) { - spawnOpts.env = Object.assign({FORCE_COLOR: supportsColor.level}, process.env) - } - - var child = spawnChild(cmd, spawnOpts); - - if (index < prefixColors.length) { - var prefixColorPath = prefixColors[index]; - lastPrefixColor = _.get(chalk, prefixColorPath, chalk.gray.dim); - } - - var name = index < names.length ? names[index] : ''; - childrenInfo[child.pid] = { - command: cmd, - index: index, - name: name, - options: spawnOpts, - restartTries: config.restartTries, - prefixColor: lastPrefixColor - }; - return child; - }); - - var streams = toStreams(children); - - handleChildEvents(streams, children, childrenInfo); - - ['SIGINT', 'SIGTERM'].forEach(function(signal) { - process.on(signal, function() { - children.forEach(function(child) { - treeKill(child.pid, signal); - }); - }); - }); -} - -function spawnChild(cmd, options) { - var child; - try { - child = spawn(cmd, options); - } catch (e) { - logError('', chalk.gray.dim, 'Error occured when executing command: ' + cmd); - logError('', chalk.gray.dim, e.stack); - process.exit(1); - } - return child; -} - -function toStreams(children) { - // Transform all process events to rx streams - return _.map(children, function(child) { - var childStreams = { - error: Rx.Node.fromEvent(child, 'error'), - close: Rx.Node.fromEvent(child, 'close') - }; - if (!config.raw) { - childStreams.stdout = Rx.Node.fromReadableStream(child.stdout); - childStreams.stderr = Rx.Node.fromReadableStream(child.stderr); - } - - return _.reduce(childStreams, function(memo, stream, key) { - memo[key] = stream.map(function(data) { - return {child: child, data: data}; - }); - - return memo; - }, {}); - }); -} - -function handleChildEvents(streams, children, childrenInfo) { - handleClose(streams, children, childrenInfo); - handleError(streams, childrenInfo); - if (!config.raw) { - handleOutput(streams, childrenInfo, 'stdout'); - handleOutput(streams, childrenInfo, 'stderr'); - } -} - -function handleOutput(streams, childrenInfo, source) { - var sourceStreams = _.map(streams, source); - var combinedSourceStream = Rx.Observable.merge.apply(this, sourceStreams); - - combinedSourceStream.subscribe(function(event) { - var prefix = getPrefix(childrenInfo, event.child); - var prefixColor = childrenInfo[event.child.pid].prefixColor; - log(prefix, prefixColor, event.data.toString()); - }); -} - -function handleClose(streams, children, childrenInfo) { - var aliveChildren = _.clone(children); - var exitCodes = []; - var closeStreams = _.map(streams, 'close'); - var closeStream = Rx.Observable.merge.apply(this, closeStreams); - var othersKilled = false - - // TODO: Is it possible that amount of close events !== count of spawned? - closeStream.subscribe(function(event) { - var exitCode = event.data; - var nonSuccess = exitCode !== 0; - exitCodes.push(exitCode); - - var prefix = getPrefix(childrenInfo, event.child); - var childInfo = childrenInfo[event.child.pid]; - var prefixColor = childInfo.prefixColor; - var command = childInfo.command; - logEvent(prefix, prefixColor, command + ' exited with code ' + exitCode); - - aliveChildren = _.filter(aliveChildren, function(child) { - return child.pid !== event.child.pid; - }); - - if (nonSuccess && config.allowRestart && childInfo.restartTries--) { - respawnChild(event, childrenInfo); - return; - } - - if (aliveChildren.length === 0) { - exit(exitCodes); - } - if (!othersKilled) { - if (config.killOthers) { - killOtherProcesses(aliveChildren); - othersKilled = true; - } else if (config.killOthersOnFail && nonSuccess) { - killOtherProcesses(aliveChildren); - othersKilled = true; - } - } - }); -} - -function respawnChild(event, childrenInfo) { - setTimeout(function() { - var childInfo = childrenInfo[event.child.pid]; - var prefix = getPrefix(childrenInfo, event.child); - var prefixColor = childInfo.prefixColor; - logEvent(prefix, prefixColor, childInfo.command + ' restarted'); - var newChild = spawnChild(childInfo.command, childInfo.options); - - childrenInfo[newChild.pid] = childrenInfo[event.child.pid]; - delete childrenInfo[event.child.pid]; - - var children = [newChild]; - var streams = toStreams(children); - handleChildEvents(streams, children, childrenInfo); - }, config.restartAfter); -} - -function killOtherProcesses(processes) { - logEvent('--> ', chalk.gray.dim, 'Sending SIGTERM to other processes..'); - - // Send SIGTERM to alive children - _.each(processes, function(child) { - treeKill(child.pid, 'SIGTERM'); - }); -} - -function exit(childExitCodes) { - var success; - switch (config.success) { - case 'first': - success = _.first(childExitCodes) === 0; - break; - case 'last': - success = _.last(childExitCodes) === 0; - break; - default: - success = _.every(childExitCodes, function(code) { - return code === 0; - }); - } - process.exit(success ? 0 : 1); -} - -function handleError(streams, childrenInfo) { - // Output emitted errors from child process - var errorStreams = _.map(streams, 'error'); - var processErrorStream = Rx.Observable.merge.apply(this, errorStreams); - - processErrorStream.subscribe(function(event) { - var command = childrenInfo[event.child.pid].command; - logError('', chalk.gray.dim, 'Error occured when executing command: ' + command); - logError('', chalk.gray.dim, event.data.stack); - }); -} - -function colorText(text, color) { - if (!config.color) { - return text; - } else { - return color(text); - } -} - -function getPrefix(childrenInfo, child) { - var prefixes = getPrefixes(childrenInfo, child); - if (_.includes(_.keys(prefixes), config.prefix)) { - return '[' + prefixes[config.prefix] + '] '; - } - - return _.reduce(prefixes, function(memo, val, key) { - var re = new RegExp('{' + key + '}', 'g'); - return memo.replace(re, val); - }, config.prefix) + ' '; -} - -function getPrefixes(childrenInfo, child) { - var prefixes = {}; - - prefixes.none = ''; - prefixes.pid = child.pid; - prefixes.index = childrenInfo[child.pid].index; - prefixes.name = childrenInfo[child.pid].name; - prefixes.time = formatDate(Date.now(), config.timestampFormat); - - var command = childrenInfo[child.pid].command; - prefixes.command = shortenText(command, config.prefixLength); - return prefixes; -} - -function shortenText(text, length, cut) { - if (text.length <= length) { - return text; - } - cut = _.isString(cut) ? cut : '..'; - - var endLength = Math.floor(length / 2); - var startLength = length - endLength; - - var first = text.substring(0, startLength); - var last = text.substring(text.length - endLength, text.length); - return first + cut + last; -} - -function log(prefix, prefixColor, text) { - logWithPrefix(prefix, prefixColor, text); -} - -function logEvent(prefix, prefixColor, text) { - if (config.raw) return; - - logWithPrefix(prefix, prefixColor, text, chalk.gray.dim); -} - -function logError(prefix, prefixColor, text) { - // This is for now same as log, there might be separate colors for stderr - // and stdout - logWithPrefix(prefix, prefixColor, text, chalk.red.bold); -} - -function logWithPrefix(prefix, prefixColor, text, color) { - var lastChar = text[text.length - 1]; - if (config.raw) { - if (lastChar !== '\n') { - text += '\n'; - } - - process.stdout.write(text); - return; - } - - if (lastChar === '\n') { - // Remove extra newline from the end to prevent extra newlines in input - text = text.slice(0, text.length - 1); - } - - var lines = text.split('\n'); - // Do not bgColor trailing space - var coloredPrefix = colorText(prefix.replace(/ $/, ''), prefixColor) + ' '; - var paddedLines = _.map(lines, function(line, i) { - var coloredLine = color ? colorText(line, color) : line; - return coloredPrefix + coloredLine; - }); - - console.log(paddedLines.join('\n')); -} - -main(); +main(); \ No newline at end of file diff --git a/test/support/sleep.js b/test/support/sleep.js new file mode 100644 index 00000000..d6147bd9 --- /dev/null +++ b/test/support/sleep.js @@ -0,0 +1 @@ +new Promise(resolve => setTimeout(resolve, parseInt(process.argv[2], 10) * 1000)).then(() => process.exit(0)); \ No newline at end of file diff --git a/test/test-functional.js b/test/test-functional.js index 204dfc3d..f1fea4e6 100644 --- a/test/test-functional.js +++ b/test/test-functional.js @@ -4,6 +4,7 @@ var path = require('path'); var assert = require('assert'); var run = require('./utils').run; var IS_WINDOWS = /^win/.test(process.platform); +var concurrently = require('../src/api.js'); // Note: Set the DEBUG_TESTS environment variable to `true` to see output of test commands. @@ -46,14 +47,14 @@ describe('concurrently', function() { }); it('--kill-others should kill other commands if one dies', () => { - return run('node ./src/main.js --kill-others "sleep 1" "echo test" "sleep 0.1 && nosuchcmd"') + return run('node ./src/main.js --kill-others "node ./test/support/sleep.js 1" "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') .then(function(exitCode) { assert.notStrictEqual(exitCode, 0); }); }); it('--kill-others-on-fail should kill other commands if one exits with non-zero status code', () => { - return run('node ./src/main.js --kill-others-on-fail "sleep 1" "exit 1" "sleep 1"') + return run('node ./src/main.js --kill-others-on-fail "node ./test/support/sleep.js 1" "exit 1" "node ./test/support/sleep.js 1"') .then(function(exitCode) { assert.notStrictEqual(exitCode, 0); }); @@ -76,7 +77,7 @@ describe('concurrently', function() { } } }).then(function() { - if(sigtermInOutput) { + if (sigtermInOutput) { done(new Error('There was a "SIGTERM" in console output')); } else if (exits !== 3) { done(new Error('There was wrong number of echoes(' + exits + ') from executed commands')); @@ -87,7 +88,7 @@ describe('concurrently', function() { }); it('--success=first should return first exit code', () => { - return run('node ./src/main.js -k --success first "echo test" "sleep 0.1 && nosuchcmd"') + return run('node ./src/main.js -k --success first "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') // When killed, sleep returns null exit code .then(function(exitCode) { assert.strictEqual(exitCode, 0); @@ -96,7 +97,7 @@ describe('concurrently', function() { it('--success=last should return last exit code', () => { // When killed, sleep returns null exit code - return run('node ./src/main.js -k --success last "echo test" "sleep 0.1 && nosuchcmd"') + return run('node ./src/main.js -k --success last "echo test" "node ./test/support/sleep.js 0.1 && nosuchcmd"') .then(function(exitCode) { assert.notStrictEqual(exitCode, 0); }); @@ -120,12 +121,12 @@ describe('concurrently', function() { var collectedLines = [] return run('node ./src/main.js "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } } - } - }) + }) .then(function(exitCode) { assert.strictEqual(exitCode, 0); @@ -141,12 +142,12 @@ describe('concurrently', function() { var collectedLines = [] return run('node ./src/main.js -n aa,bb "echo one" "echo two"', { - onOutputLine: (line) => { - if (/(one|two)$/.exec(line)) { - collectedLines.push(line) + onOutputLine: (line) => { + if (/(one|two)$/.exec(line)) { + collectedLines.push(line) + } } - } - }) + }) .then(function(exitCode) { assert.strictEqual(exitCode, 0); @@ -163,7 +164,7 @@ describe('concurrently', function() { var exitedWithOne = false; var restarted = false; - run('node ./src/main.js --allow-restart "sleep 0.1 && exit 1" "sleep 1"', { + run('node ./src/main.js --allow-restart "node ./test/support/sleep.js 0.1 && exit 1" "node ./test/support/sleep.js 1"', { pipe: false, onOutputLine: (line) => { var re = /exited with code (.+)/.exec(line); @@ -188,7 +189,7 @@ describe('concurrently', function() { var readline = require('readline'); var start, end; - run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "sleep 1"', { + run('node ./src/main.js --allow-restart --restart-after 300 "exit 1" "node ./test/support/sleep.js 1"', { pipe: false, onOutputLine: (line) => { if (!start && /exited with code (.+)/.test(line)) { @@ -204,15 +205,16 @@ describe('concurrently', function() { if (end - start >= 300 && end - start < 400) { done(); } else { - done(new Error('No restarted process after 300 miliseconds')); + done(new Error('No restarted process after 300 miliseconds - delta is: ' + (end - start))); } }); }); + it('--restart-tries=n should restart a proccess at most n times', (done) => { var readline = require('readline'); var restartedTimes = 0; - run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "sleep 1"', { + run('node ./src/main.js --allow-restart --restart-tries 2 "exit 1" "node ./test/support/sleep.js 1"', { pipe: false, onOutputLine: (line) => { if (/restarted/.test(line)) { @@ -228,49 +230,77 @@ describe('concurrently', function() { }); }); - ['SIGINT', 'SIGTERM'].forEach((signal) => { - if (IS_WINDOWS) { - console.log('IS_WINDOWS=true'); - console.log('Skipping SIGINT/SIGTERM propagation tests ..'); - return; - } - - it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { - var readline = require('readline'); - var waitingStart = 2; - var waitingSignal = 2; - - function waitForSignal(cb) { - if (waitingSignal) { - setTimeout(waitForSignal, 100); - } else { - cb(); - } - } - - run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { - onOutputLine: function(line, child) { - // waiting for startup - if (/STARTED/.test(line)) { - waitingStart--; - } - if (!waitingStart) { - // both processes are started - child.kill(signal); - } + if (IS_WINDOWS) { + console.log('IS_WINDOWS=true'); + console.log('Skipping SIGINT/SIGTERM propagation tests ...'); + } else { + ['SIGINT', 'SIGTERM'].forEach((signal) => { + it('killing it with ' + signal + ' should propagate the signal to the children', function(done) { + var readline = require('readline'); + var waitingStart = 2; + var waitingSignal = 2; + + function waitForSignal(cb) { + if (waitingSignal) { + setTimeout(waitForSignal, 100); + } else { + cb(); + } + } - // waiting for signal - if (new RegExp(signal).test(line)) { - waitingSignal--; - } - } - }).then(function() { - waitForSignal(done); + run('node ./src/main.js "node ./test/support/signal.js" "node ./test/support/signal.js"', { + onOutputLine: function(line, child) { + // waiting for startup + if (/STARTED/.test(line)) { + waitingStart--; + } + if (!waitingStart) { + // both processes are started + child.kill(signal); + } + + // waiting for signal + if (new RegExp(signal).test(line)) { + waitingSignal--; + } + } + }).then(function() { + waitForSignal(done); + }); + }); + }); + } + + describe('api', () => { + // TODO: hide output from echo command if DEBUG_TESTS != true + it('should run and return correct exit codes', () => { + var commands = ['echo 1', 'exit 2']; + return concurrently(commands) + .then(function(result) { + assert(result.length === 2); + assert(result[0] === 0); + assert(result[1] === 2); + }); + }); + it('should use names', () => { + // not sure how to test colors except visually ... + var commands = [{ + name: 'echo', + prefixModifier: 'dim', + prefixTextColor: 'yellow', + prefixBackColor: 'bgGreen', + command: 'echo 1' + }, 'exit 2']; + return concurrently(commands) + .then(function(result) { + assert(result.length === 2); + assert(result[0] === 0); + assert(result[1] === 2); + }); }); - }); }); }); function resolve(relativePath) { return path.join(testDir, relativePath); -} +} \ No newline at end of file diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 00000000..994f3721 --- /dev/null +++ b/typings.d.ts @@ -0,0 +1,54 @@ +declare module "concurrently" { + + export interface Options { + // Kill other processes if one dies + killOthers?: boolean, + + // Kill other processes if one exits with non zero status code + killOthersOnFail?: boolean, + + // Return success or failure of the 'first' child to terminate, the 'last' child, + // or succeed only if 'all' children succeed + success?: 'first' | 'last' | 'all', + + // Prefix logging with pid + prefix?: '' | 'pid' | 'none' | 'time' | 'command' | 'index' | 'name', + + // moment/date-fns format + timestampFormat?: string, + + // How many characters to display from start of command in prefix if + // command is defined. Note that also '..' will be added in the middle + prefixLength?: number, + + // By default, color output + color?: boolean, + + // If true, the output will only be raw output of processes, nothing more + raw?: boolean, + + // If true, the process restart when it exited with status code non-zero + allowRestart?: boolean, + + // By default, restart instantly + restartAfter?: number, + + // By default, restart once + restartTries?: number + } + + // see https://github.com/chalk/chalk#styles for details + export type modifier = 'reset' | 'bold' | 'dim' | 'italic' | 'underline' | 'inverse' | 'hidden' | 'strikethrough'; + export type textColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray' | 'blackBright' | 'redBright' | 'greenBright' | 'yellowBright' | 'blueBright' | 'magentaBright' | 'cyanBright' | 'whiteBright'; + export type backColor = 'bgBlack' | 'bgRed' | 'bgGreen' | 'bgYellow' | 'bgBlue' | 'bgMagenta' | 'bgCyan' | 'bgWhite' | 'bgBlackBright' | 'bgRedBright' | 'bgGreenBright' | 'bgYellowBright' | 'bgBlueBright' | 'bgMagentaBright' | 'bgCyanBright' | 'bgWhiteBright'; + + export interface Command { + name?: string, + prefixModifier?: modifier, + prefixTextColor?: textColor, + prefixBackColor?: backColor, + command: string, + } + + export function run(commands: (string | Command)[], options?: Options): Promise; +}