Skip to content

Commit 00f674a

Browse files
committed
New: --print-label option (fixes #23)
1 parent e5f1a90 commit 00f674a

File tree

12 files changed

+437
-64
lines changed

12 files changed

+437
-64
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ Usage: npm-run-all [...tasks] [OPTIONS]
4343
4444
-c, --continue-on-error Set the flag to ignore errors to the current
4545
group of tasks.
46+
-l, --print-label Set the flag to print the task name as a prefix
47+
on each line of output, to the current group of
48+
tasks.
4649
--silent Set "silent" to the log level of npm.
4750
4851
-p, --parallel [...tasks] Run a group of tasks in parallel.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
},
2323
"dependencies": {
2424
"babel-runtime": "^6.3.13",
25+
"chalk": "^1.1.3",
2526
"cross-spawn-async": "^2.1.9",
2627
"minimatch": "^3.0.0",
2728
"ps-tree": "^1.0.1",
28-
"shell-quote": "^1.4.3"
29+
"shell-quote": "^1.4.3",
30+
"string.prototype.padend": "^3.0.0"
2931
},
3032
"devDependencies": {
3133
"babel-cli": "^6.4.0",

src/bin/help.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ Usage: npm-run-all [...tasks] [OPTIONS]
2323
2424
-c, --continue-on-error Set the flag to ignore errors to the current
2525
group of tasks.
26+
-l, --print-label Set the flag to print the task name as a prefix
27+
on each line of output, to the current group of
28+
tasks.
2629
--silent Set "silent" to the log level of npm.
2730
2831
-p, --parallel [...tasks] Run a group of tasks in parallel.

src/bin/main.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function parse(args) {
6565
const groups = [{
6666
parallel: false,
6767
continueOnError: false,
68+
printLabel: false,
6869
patterns: []
6970
}];
7071

@@ -79,6 +80,7 @@ function parse(args) {
7980
groups.push({
8081
parallel: false,
8182
continueOnError: arg === "-S",
83+
printLabel: false,
8284
patterns: []
8385
});
8486
break;
@@ -89,6 +91,7 @@ function parse(args) {
8991
groups.push({
9092
parallel: true,
9193
continueOnError: arg === "-P",
94+
printLabel: false,
9295
patterns: []
9396
});
9497
break;
@@ -98,6 +101,11 @@ function parse(args) {
98101
groups[groups.length - 1].continueOnError = true;
99102
break;
100103

104+
case "-l":
105+
case "--print-label":
106+
groups[groups.length - 1].printLabel = true;
107+
break;
108+
101109
case "--silent":
102110
// do nothing.
103111
break;
@@ -145,7 +153,7 @@ export default function npmRunAll(args, stdout, stderr) {
145153
const {groups, packageConfig} = parse(args);
146154

147155
return groups.reduce(
148-
(prev, {patterns, parallel, continueOnError}) => {
156+
(prev, {patterns, parallel, continueOnError, printLabel}) => {
149157
if (patterns.length === 0) {
150158
return prev;
151159
}
@@ -157,6 +165,7 @@ export default function npmRunAll(args, stdout, stderr) {
157165
stdin,
158166
parallel,
159167
continueOnError,
168+
printLabel,
160169
packageConfig,
161170
silent
162171
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @author Toru Nagashima
3+
* @copyright 2016 Toru Nagashima. All rights reserved.
4+
* See LICENSE file in root directory for full license.
5+
*/
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
import {Transform} from "stream";
12+
13+
//------------------------------------------------------------------------------
14+
// Helpers
15+
//------------------------------------------------------------------------------
16+
17+
const ALL_BR = /\n/g;
18+
19+
/**
20+
* The transform stream to insert a specific prefix.
21+
*
22+
* Several streams can exist for the same output stream.
23+
* This stream will insert the prefix if the last output came from other instance.
24+
* To do that, this stream is using a shared state object.
25+
*
26+
* @private
27+
*/
28+
class PrefixTransform extends Transform {
29+
/**
30+
* @param {string} prefix - A prefix text to be inserted.
31+
* @param {object} state - A state object.
32+
* @param {string} state.lastPrefix - The last prefix which is printed.
33+
* @param {boolean} state.lastIsLinebreak -The flag to check whether the last output is a line break or not.
34+
*/
35+
constructor(prefix, state) {
36+
super();
37+
38+
this.prefix = prefix;
39+
this.state = state;
40+
}
41+
42+
/**
43+
* Transforms the output chunk.
44+
*
45+
* @param {string|Buffer} chunk - A chunk to be transformed.
46+
* @param {string} encoding - The encoding of the chunk.
47+
* @param {function} callback - A callback function that is called when done.
48+
* @returns {void}
49+
*/
50+
_transform(chunk, encoding, callback) {
51+
const prefix = this.prefix;
52+
const nPrefix = `\n${prefix}`;
53+
const state = this.state;
54+
const firstPrefix =
55+
state.lastIsLinebreak ? prefix :
56+
(state.lastPrefix !== prefix) ? "\n" :
57+
/* otherwise */ "";
58+
const prefixed = `${firstPrefix}${chunk}`.replace(ALL_BR, nPrefix);
59+
const index = prefixed.indexOf(prefix, Math.max(0, prefixed.length - prefix.length));
60+
61+
state.lastPrefix = prefix;
62+
state.lastIsLinebreak = (index !== -1);
63+
64+
callback(null, (index !== -1) ? prefixed.slice(0, index) : prefixed);
65+
}
66+
}
67+
68+
//------------------------------------------------------------------------------
69+
// Public API
70+
//------------------------------------------------------------------------------
71+
72+
/**
73+
* Create a transform stream to insert the specific prefix.
74+
*
75+
* Several streams can exist for the same output stream.
76+
* This stream will insert the prefix if the last output came from other instance.
77+
* To do that, this stream is using a shared state object.
78+
*
79+
* @param {string} prefix - A prefix text to be inserted.
80+
* @param {object} state - A state object.
81+
* @param {string} state.lastPrefix - The last prefix which is printed.
82+
* @param {boolean} state.lastIsLinebreak -The flag to check whether the last output is a line break or not.
83+
* @returns {stream.Transform} The created transform stream.
84+
*/
85+
export default function createPrefixTransform(prefix, state) {
86+
return new PrefixTransform(prefix, state);
87+
}

src/lib/npm-run-all.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ function toOverwriteOptions(config) {
4646
return options;
4747
}
4848

49+
/**
50+
* Gets the maximum length.
51+
*
52+
* @param {number} length - The current maximum length.
53+
* @param {string} name - A name.
54+
* @returns {number} The maximum length.
55+
*/
56+
function maxLength(length, name) {
57+
return Math.max(name.length, length);
58+
}
59+
4960
/**
5061
* Runs npm-scripts which are matched with given patterns.
5162
*
@@ -90,6 +101,9 @@ function toOverwriteOptions(config) {
90101
* @param {boolean} options.continueOnError -
91102
* The flag to ignore errors.
92103
* Default is `false`.
104+
* @param {boolean} options.printLabel -
105+
* The flag to print task names at the head of each line.
106+
* Default is `false`.
93107
* @returns {Promise}
94108
* A promise object which becomes fullfilled when all npm-scripts are completed.
95109
*/
@@ -103,7 +117,8 @@ export default function npmRunAll(
103117
taskList = null,
104118
packageConfig = null,
105119
silent = false,
106-
continueOnError = false
120+
continueOnError = false,
121+
printLabel = false
107122
} = {}
108123
) {
109124
try {
@@ -124,8 +139,21 @@ export default function npmRunAll(
124139
}
125140

126141
const tasks = matchTasks(taskList || readTasks(), patterns);
142+
const labelWidth = tasks.reduce(maxLength, 0);
127143
const runTasks = parallel ? runTasksInParallel : runTasksInSequencial;
128-
return runTasks(tasks, stdin, stdout, stderr, prefixOptions, continueOnError);
144+
return runTasks(tasks, {
145+
stdin,
146+
stdout,
147+
stderr,
148+
prefixOptions,
149+
continueOnError,
150+
labelState: {
151+
enabled: printLabel,
152+
width: labelWidth,
153+
lastPrefix: null,
154+
lastIsLinebreak: true
155+
}
156+
});
129157
}
130158
catch (err) {
131159
return Promise.reject(new Error(err.message));

src/lib/run-task.js

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,55 @@
33
* @copyright 2015 Toru Nagashima. All rights reserved.
44
* See LICENSE file in root directory for full license.
55
*/
6+
7+
//------------------------------------------------------------------------------
8+
// Requirements
9+
//------------------------------------------------------------------------------
10+
11+
import chalk from "chalk";
612
import {parse as parseArgs} from "shell-quote";
13+
import padEnd from "string.prototype.padend";
14+
import createPrefixTransform from "./create-prefix-transform-stream";
715
import spawn from "./spawn";
816

17+
//------------------------------------------------------------------------------
18+
// Helpers
19+
//------------------------------------------------------------------------------
20+
21+
/**
22+
* Pass through the argument.
23+
*
24+
* @param {string} s - An argument.
25+
* @returns {string} The argument.
26+
*/
27+
function identity(s) {
28+
return s;
29+
}
30+
31+
/**
32+
* Wraps stdout/stderr with a transform stream to add the task name as prefix.
33+
*
34+
* @param {string} taskName - The task name.
35+
* @param {stream.Writable} source - An output stream to be wrapped.
36+
* @param {stream.Writable} std - process.stdout/process.stderr.
37+
* @param {object} labelState - An label state for the transform stream.
38+
* @returns {stream.Writable} `source` or the created wrapped stream.
39+
*/
40+
function wrapLabeling(taskName, source, std, labelState) {
41+
if (source == null || !labelState.enabled) {
42+
return source;
43+
}
44+
45+
const label = padEnd(taskName, labelState.width);
46+
const color = (source === std) ? chalk.gray : identity;
47+
const prefix = color(`[${label}] `);
48+
const stream = createPrefixTransform(prefix, labelState);
49+
50+
stream.pipe(source);
51+
52+
return stream;
53+
}
54+
955
/**
1056
* Converts a given stream to an option for `child_process.spawn`.
1157
*
@@ -22,37 +68,54 @@ function detectStreamKind(stream, std) {
2268
);
2369
}
2470

71+
//------------------------------------------------------------------------------
72+
// Interface
73+
//------------------------------------------------------------------------------
74+
2575
/**
2676
* Run a npm-script of a given name.
2777
* The return value is a promise which has an extra method: `abort()`.
2878
* The `abort()` kills the child process to run the npm-script.
2979
*
3080
* @param {string} task - A npm-script name to run.
31-
* @param {stream.Readable|null} stdin -
81+
* @param {object} options - An option object.
82+
* @param {stream.Readable|null} options.stdin -
3283
* A readable stream to send messages to stdin of child process.
3384
* If this is `null`, ignores it.
3485
* If this is `process.stdin`, inherits it.
3586
* Otherwise, makes a pipe.
36-
* @param {stream.Writable|null} stdout -
87+
* @param {stream.Writable|null} options.stdout -
3788
* A writable stream to receive messages from stdout of child process.
3889
* If this is `null`, cannot send.
3990
* If this is `process.stdout`, inherits it.
4091
* Otherwise, makes a pipe.
41-
* @param {stream.Writable|null} stderr -
92+
* @param {stream.Writable|null} options.stderr -
4293
* A writable stream to receive messages from stderr of child process.
4394
* If this is `null`, cannot send.
4495
* If this is `process.stderr`, inherits it.
4596
* Otherwise, makes a pipe.
46-
* @param {string[]} prefixOptions -
97+
* @param {string[]} options.prefixOptions -
4798
* An array of options which are inserted before the task name.
99+
* @param {object} options.labelState - A state object for printing labels.
48100
* @returns {Promise}
49101
* A promise object which becomes fullfilled when the npm-script is completed.
50102
* This promise object has an extra method: `abort()`.
51103
* @private
52104
*/
53-
export default function runTask(task, stdin, stdout, stderr, prefixOptions) {
105+
export default function runTask(
106+
task,
107+
{
108+
stdin,
109+
stdout: sourceStdout,
110+
stderr: sourceStderr,
111+
prefixOptions,
112+
labelState
113+
}
114+
) {
54115
let cp = null;
55116
const promise = new Promise((resolve, reject) => {
117+
const stdout = wrapLabeling(task, sourceStdout, process.stdout, labelState);
118+
const stderr = wrapLabeling(task, sourceStderr, process.stderr, labelState);
56119
const stdinKind = detectStreamKind(stdin, process.stdin);
57120
const stdoutKind = detectStreamKind(stdout, process.stdout);
58121
const stderrKind = detectStreamKind(stderr, process.stderr);
@@ -66,8 +129,8 @@ export default function runTask(task, stdin, stdout, stderr, prefixOptions) {
66129

67130
// Piping stdio.
68131
if (stdinKind === "pipe") { stdin.pipe(cp.stdin); }
69-
if (stdoutKind === "pipe") { cp.stdout.pipe(stdout); }
70-
if (stderrKind === "pipe") { cp.stderr.pipe(stderr); }
132+
if (stdoutKind === "pipe") { cp.stdout.pipe(stdout, {end: false}); }
133+
if (stderrKind === "pipe") { cp.stderr.pipe(stderr, {end: false}); }
71134

72135
// Register
73136
cp.on("error", (err) => {

0 commit comments

Comments
 (0)