Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### v3.2.0
- [#31](https://github.com/darkguy2008/parallelshell/issues/31) Add
`-n, --npm <pattern>` to run npm scripts with wildcard matching,
mixed with ordinary commands. Validate all selections before launching.

### v3.1.0
- [#40](https://github.com/darkguy2008/parallelshell/issues/40) Add
`-t, --timeout <seconds>` to stop remaining commands after a deadline,
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,30 @@ Available options:
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-t, --timeout <seconds> stop remaining commands after the deadline
-n, --npm <pattern> run matching npm scripts from package.json

```

Use `-n` (or `--npm`) before each npm script name or pattern:

```bash
parallelshell -n "build:*" "echo ordinary command" -n "test:{unit,integration}"
```

Scripts are read from `package.json` in the current directory and run through
`npm run`, including npm's pre/post lifecycle scripts. npm must be on PATH.
Exact script names take precedence over patterns. Patterns use minimatch syntax:
`*`, `?`, character classes, braces and extended globs such as `build:+(js|css)`.
Matching is case-sensitive, includes names starting with a dot and treats leading
`#` and `!` literally. `/` separates pattern segments; use `**` across segments.
Quote patterns with double quotes so they work on Windows and are not expanded
by your shell. Matches follow package.json order; repeated selections run again.
All selections are validated before any command starts. Missing values, unmatched
patterns and missing or invalid package.json files exit with code 1.

This shorthand uses the existing shutdown behavior. The npm-script shutdown
limitation on Debian/Ubuntu tracked in #22 remains unchanged.

Use `--timeout` (or `-t`) to limit the overall run, including when `--wait` is
enabled:

Expand Down
46 changes: 42 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

'use strict';
var spawn = require('child_process').spawn;
var spawnNpm = require('cross-spawn');
var fs = require('fs');
var minimatch = require('minimatch');
var path = require('path');
var signals = require('constants');
var SIGNAL_EXIT_CODE_BASE = 128;
Expand All @@ -13,12 +16,36 @@ var MAX_TIMEOUT_MS = Math.pow(2, 31) - 1;

var WINDOWS = process.platform === 'win32';
var commandPrefix = WINDOWS ? '' : 'exec ';
var children, args, wait, cmds, verbose, timeout, timer, i ,len;
var children, args, wait, cmds, verbose, timeout, timer, scripts, i ,len;
cmds = [];
args = process.argv.slice(2);
for (i = 0, len = args.length; i < len; i++) {
if (args[i][0] === '-') {
switch (args[i]) {
case '-n':
case '--npm':
var pattern = args[++i];
if (!pattern || pattern[0] === '-') {
console.error('--npm requires a script name or pattern');
process.exit(1);
}
try {
if (!scripts) {
scripts = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')).scripts;
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) throw new Error('package.json must contain a scripts object');
}
var names = Object.keys(scripts);
var matches = Object.prototype.hasOwnProperty.call(scripts, pattern) ? [pattern] : minimatch.match(names, pattern, { dot: true, nocomment: true, nonegate: true, allowWindowsEscape: true });
if (!matches.length) throw new Error('no npm scripts match ' + JSON.stringify(pattern));
matches.forEach(function (name) {
if (typeof scripts[name] !== 'string') throw new Error('npm script ' + JSON.stringify(name) + ' must be a string');
cmds.push({ script: name });
});
} catch (error) {
console.error('--npm: ' + error.message);
process.exit(1);
}
break;
case '-t':
case '--timeout':
timeout = Number(args[++i]) * MILLISECONDS_PER_SECOND;
Expand All @@ -41,6 +68,7 @@ for (i = 0, len = args.length; i < len; i++) {
console.log('-v, --verbose verbose logging')
console.log('-w, --wait will not close sibling processes on error')
console.log('-t, --timeout <seconds> stop remaining commands after the deadline');
console.log('-n, --npm <pattern> run matching npm scripts from package.json');
process.exit();
break;
}
Expand Down Expand Up @@ -134,10 +162,20 @@ FORWARDED_SIGNALS.forEach(function (signal) {
});

children = cmds.map(function (cmd) {
var command = commandPrefix + cmd;
var child = spawn(command, {
shell: true,
var command = cmd.script === undefined ? commandPrefix + cmd : 'npm run -- ' + JSON.stringify(cmd.script);
var options = {
stdio: ['pipe', process.stdout, process.stderr]
};
var child;
if (cmd.script === undefined) {
options.shell = true;
child = spawn(command, options);
} else {
child = spawnNpm('npm', ['run', '--', cmd.script], options);
}
child.on('error', function (error) {
console.error(error.message);
childClose.call(this, 1);
}).on('close', childClose);
child.cmd = command;
return child;
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parallelshell",
"version": "3.1.0",
"version": "3.2.0",
"description": "Invoke multiple commands, running in parallel",
"homepage": "https://github.com/darkguy2008/parallelshell",
"repository": {
Expand All @@ -27,6 +27,10 @@
],
"author": "DARKGuy <darkguy2008@gmail.com>",
"license": "MIT",
"dependencies": {
"cross-spawn": "^6.0.6",
"minimatch": "^3.1.5"
},
"devDependencies": {
"chai": "^4.0.2",
"coffee-script": "^1.12.6",
Expand Down
108 changes: 108 additions & 0 deletions test/index.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ usageInfo = """
-v, --verbose verbose logging
-w, --wait will not close sibling processes on error
-t, --timeout <seconds> stop remaining commands after the deadline
-n, --npm <pattern> run matching npm scripts from package.json
""" + "\n"

spawned = []
Expand Down Expand Up @@ -115,9 +116,116 @@ afterEach ->
for directory in triggerDirectories.splice(0)
trigger = path.join directory, TRIGGER_NAME
fs.unlinkSync trigger if fs.existsSync trigger
manifest = path.join directory, "package.json"
fs.unlinkSync manifest if fs.existsSync manifest
npmLog = path.join directory, "npm-debug.log"
fs.unlinkSync npmLog if fs.existsSync npmLog
fs.rmdirSync directory

describe "parallelshell", ->
describe "npm scripts", ->
scriptNames = ["build:js", "build:css", "build:html", "test:js", ".hidden", "#hash", "!bang", "literal*", "two words", "quote\"name", "amp&name", "percent%PATH%", "nested/build/js"]

project = (scripts) ->
directory = path.dirname newTrigger()
fs.writeFileSync path.join(directory, "package.json"), JSON.stringify {name: "parallelshell-fixture", version: "1.0.0", scripts}
directory

npmProject = ->
scripts = {}
for name in scriptNames
scripts[name] = "node -e \"console.log('script '+process.env.npm_lifecycle_event)\""
project scripts

runScripts = (args...) -> spawnParallelshellWith {cwd: npmProject()}, args...

["-n", "--npm"].forEach (flag) ->
it "should run an exact npm script with #{flag}", ->
ps = runScripts flag, "build:js"
ps.exited.then (result) ->
result.code.should.equal 0, ps.output + ps.errorOutput
outputLines(ps).should.include "script build:js"

patterns = {
"build:*": ["build:js", "build:css", "build:html"]
"*:js": ["build:js", "test:js"]
"b*:j?": ["build:js"]
"build:{js,css}": ["build:js", "build:css"]
"build:[ch]*": ["build:css", "build:html"]
"build:+(js|css)": ["build:js", "build:css"]
"literal*": ["literal*"]
".*": [".hidden"]
"#hash": ["#hash"]
"!bang": ["!bang"]
"two words": ["two words"]
"quote\"name": ["quote\"name"]
"amp&name": ["amp&name"]
"percent%PATH%": ["percent%PATH%"]
"nested/**": ["nested/build/js"]
}
Object.keys(patterns).forEach (pattern) ->
it "should expand #{pattern} against script names", ->
ps = runScripts "-n", pattern
ps.exited.then (result) ->
result.code.should.equal 0, ps.output + ps.errorOutput
outputLines(ps).filter((line) -> line.indexOf("script ") == 0).sort().should.deep.equal patterns[pattern].map((name) -> "script #{name}").sort()

it "should mix repeated npm options with ordinary commands and existing options", ->
ps = runScripts "-w", "-n", "build:js", "echo ordinary", "--npm", "build:css", "-t", String(COMPLETION_TIMEOUT_SECONDS)
ps.exited.then (result) ->
result.code.should.equal 0
outputLines(ps).should.include.members ["ordinary", "script build:js", "script build:css"]

it "should preserve explicitly repeated scripts", ->
ps = runScripts "-n", "build:js", "-n", "build:js"
ps.exited.then (result) ->
result.code.should.equal 0
outputLines(ps).filter((line) -> line == "script build:js").length.should.equal 2

it "should preserve npm pre and post lifecycle scripts", ->
ps = spawnParallelshellWith {cwd: project({prebuild: "echo lifecycle-pre", build: "echo lifecycle-main", postbuild: "echo lifecycle-post"})}, "-n", "build"
ps.exited.then (result) ->
result.code.should.equal 0
outputLines(ps).filter((line) -> line.indexOf("lifecycle-") == 0).should.deep.equal ["lifecycle-pre", "lifecycle-main", "lifecycle-post"]

it "should preserve npm failure status", ->
ps = spawnParallelshellWith {cwd: project({fail: exitProcess(FAILURE_EXIT_CODE)})}, "-n", "fail"
ps.exited.then (result) -> result.code.should.not.equal 0

[undefined, "", "-w", "missing", "missing:*"].forEach (pattern) ->
it "should reject #{pattern} before launching any command", ->
args = ["echo ordinary", "-n", "build:js", "-n"]
args.push pattern if pattern != undefined
ps = runScripts args...
ps.exited.then (result) ->
result.code.should.equal 1
ps.output.should.equal ""
ps.errorOutput.should.contain "--npm"

[null, [], {bad: 42}].forEach (scripts) ->
it "should reject missing or invalid script definitions #{JSON.stringify scripts}", ->
ps = spawnParallelshellWith {cwd: project(scripts)}, "echo ordinary", "-n", "bad"
ps.exited.then (result) ->
result.code.should.equal 1
ps.output.should.equal ""
ps.errorOutput.should.contain "--npm"

it "should reject invalid package JSON before launching commands", ->
directory = project {}
fs.writeFileSync path.join(directory, "package.json"), "{"
ps = spawnParallelshellWith {cwd: directory}, "echo ordinary", "-n", "build"
ps.exited.then (result) ->
result.code.should.equal 1
ps.output.should.equal ""
ps.errorOutput.should.contain "--npm"

it "should reject a missing package.json before launching commands", ->
ps = spawnParallelshellWith {cwd: path.dirname(newTrigger())}, "echo ordinary", "-n", "build"
ps.exited.then (result) ->
result.code.should.equal 1
ps.output.should.equal ""
ps.errorOutput.should.contain "--npm"

["-t", "--timeout"].forEach (flag) ->
it "should stop running commands at the deadline with #{flag}", ->
ps = spawnParallelshell flag, String(TIMEOUT_SECONDS), succeedingProcess, waitingProcess(), waitingProcess()
Expand Down
Loading