concurrently 6.2.2 → 6.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -16
- package/bin/concurrently.js +23 -13
- package/bin/concurrently.spec.js +64 -1
- package/bin/epilogue.txt +6 -2
- package/index.js +10 -1
- package/package.json +1 -1
- package/src/command-parser/expand-npm-wildcard.js +14 -5
- package/src/command-parser/expand-npm-wildcard.spec.js +48 -2
- package/src/command.js +14 -0
- package/src/command.spec.js +93 -1
- package/src/completion-listener.js +1 -1
- package/src/completion-listener.spec.js +1 -0
- package/src/concurrently.js +28 -16
- package/src/concurrently.spec.js +13 -0
- package/src/defaults.js +5 -1
- package/src/flow-control/base-handler.spec.js +22 -0
- package/src/flow-control/log-timings.js +64 -0
- package/src/flow-control/log-timings.spec.js +137 -0
- package/src/flow-control/restart-process.spec.js +9 -1
- package/src/logger.js +66 -1
- package/src/logger.spec.js +123 -0
package/README.md
CHANGED
|
@@ -116,21 +116,25 @@ Help:
|
|
|
116
116
|
concurrently [options] <command ...>
|
|
117
117
|
|
|
118
118
|
General
|
|
119
|
-
-m, --max-processes
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-n, --names
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-r, --raw
|
|
128
|
-
|
|
129
|
-
-s, --success
|
|
130
|
-
|
|
131
|
-
|
|
119
|
+
-m, --max-processes How many processes should run at once.
|
|
120
|
+
New processes only spawn after all restart tries of a
|
|
121
|
+
process. [number]
|
|
122
|
+
-n, --names List of custom names to be used in prefix template.
|
|
123
|
+
Example names: "main,browser,server" [string]
|
|
124
|
+
--name-separator The character to split <names> on. Example usage:
|
|
125
|
+
concurrently -n "styles|scripts|server" --name-separator
|
|
126
|
+
"|" [default: ","]
|
|
127
|
+
-r, --raw Output only raw output of processes, disables
|
|
128
|
+
prettifying and concurrently coloring. [boolean]
|
|
129
|
+
-s, --success Return exit code of zero or one based on the success or
|
|
130
|
+
failure of the "first" child to terminate, the "last
|
|
131
|
+
child", or succeed only if "all" child processes
|
|
132
|
+
succeed.
|
|
132
133
|
[choices: "first", "last", "all"] [default: "all"]
|
|
133
|
-
|
|
134
|
+
--no-color Disables colors from logging [boolean]
|
|
135
|
+
--hide Comma-separated list of processes to hide the output.
|
|
136
|
+
The processes can be identified by their name or index.
|
|
137
|
+
[string] [default: ""]
|
|
134
138
|
|
|
135
139
|
Prefix styling
|
|
136
140
|
-p, --prefix Prefix used in logging for each process.
|
|
@@ -199,6 +203,10 @@ Examples:
|
|
|
199
203
|
$ concurrently --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold"
|
|
200
204
|
"http-server" "npm run watch"
|
|
201
205
|
|
|
206
|
+
- Configuring via environment variables with CONCURRENTLY_ prefix
|
|
207
|
+
|
|
208
|
+
$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true concurrently "echo hello" "echo world"
|
|
209
|
+
|
|
202
210
|
- Send input to default
|
|
203
211
|
|
|
204
212
|
$ concurrently --handle-input "nodemon" "npm run watch-js"
|
|
@@ -218,9 +226,9 @@ Examples:
|
|
|
218
226
|
|
|
219
227
|
$ concurrently npm:watch-node npm:watch-js npm:watch-css
|
|
220
228
|
|
|
221
|
-
- Shortened NPM run command with wildcard
|
|
229
|
+
- Shortened NPM run command with wildcard (make sure to wrap it in quotes!)
|
|
222
230
|
|
|
223
|
-
$ concurrently npm:watch-*
|
|
231
|
+
$ concurrently "npm:watch-*"
|
|
224
232
|
|
|
225
233
|
For more details, visit https://github.com/open-cli-tools/concurrently
|
|
226
234
|
```
|
|
@@ -250,6 +258,9 @@ concurrently can be used programmatically by using the API documented below:
|
|
|
250
258
|
- `prefix`: the prefix type to use when logging processes output.
|
|
251
259
|
Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
|
|
252
260
|
Default: the name of the process, or its index if no name is set.
|
|
261
|
+
- `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk).
|
|
262
|
+
If concurrently would run more commands than there are colors, the last color is repeated.
|
|
263
|
+
Prefix colors specified per-command take precedence over this list.
|
|
253
264
|
- `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
|
|
254
265
|
- `raw`: whether raw mode should be used, meaning strictly process output will
|
|
255
266
|
be logged, without any prefixes, colouring or extra stuff.
|
package/bin/concurrently.js
CHANGED
|
@@ -11,6 +11,8 @@ const args = yargs
|
|
|
11
11
|
.version('v', require('../package.json').version)
|
|
12
12
|
.alias('v', 'V')
|
|
13
13
|
.alias('v', 'version')
|
|
14
|
+
// TODO: Add some tests for this.
|
|
15
|
+
.env('CONCURRENTLY')
|
|
14
16
|
.options({
|
|
15
17
|
// General
|
|
16
18
|
'm': {
|
|
@@ -55,6 +57,18 @@ const args = yargs
|
|
|
55
57
|
describe: 'Disables colors from logging',
|
|
56
58
|
type: 'boolean'
|
|
57
59
|
},
|
|
60
|
+
'hide': {
|
|
61
|
+
describe:
|
|
62
|
+
'Comma-separated list of processes to hide the output.\n' +
|
|
63
|
+
'The processes can be identified by their name or index.',
|
|
64
|
+
default: defaults.hide,
|
|
65
|
+
type: 'string'
|
|
66
|
+
},
|
|
67
|
+
'timings': {
|
|
68
|
+
describe: 'Show timing information for all processes',
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
default: defaults.timings
|
|
71
|
+
},
|
|
58
72
|
|
|
59
73
|
// Kill others
|
|
60
74
|
'k': {
|
|
@@ -135,7 +149,7 @@ const args = yargs
|
|
|
135
149
|
'Can be either the index or the name of the process.'
|
|
136
150
|
}
|
|
137
151
|
})
|
|
138
|
-
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color'], 'General')
|
|
152
|
+
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide', 'timings'], 'General')
|
|
139
153
|
.group(['p', 'c', 'l', 't'], 'Prefix styling')
|
|
140
154
|
.group(['i', 'default-input-target'], 'Input handling')
|
|
141
155
|
.group(['k', 'kill-others-on-fail'], 'Killing other processes')
|
|
@@ -144,19 +158,12 @@ const args = yargs
|
|
|
144
158
|
.epilogue(fs.readFileSync(__dirname + '/epilogue.txt', { encoding: 'utf8' }))
|
|
145
159
|
.argv;
|
|
146
160
|
|
|
147
|
-
const prefixColors = args.prefixColors.split(',');
|
|
148
161
|
const names = (args.names || '').split(args.nameSeparator);
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
command,
|
|
156
|
-
prefixColor: lastColor,
|
|
157
|
-
name: names[index]
|
|
158
|
-
};
|
|
159
|
-
}), {
|
|
163
|
+
concurrently(args._.map((command, index) => ({
|
|
164
|
+
command,
|
|
165
|
+
name: names[index]
|
|
166
|
+
})), {
|
|
160
167
|
handleInput: args.handleInput,
|
|
161
168
|
defaultInputTarget: args.defaultInputTarget,
|
|
162
169
|
killOthers: args.killOthers
|
|
@@ -164,12 +171,15 @@ concurrently(args._.map((command, index) => {
|
|
|
164
171
|
: (args.killOthersOnFail ? ['failure'] : []),
|
|
165
172
|
maxProcesses: args.maxProcesses,
|
|
166
173
|
raw: args.raw,
|
|
174
|
+
hide: args.hide.split(','),
|
|
167
175
|
prefix: args.prefix,
|
|
176
|
+
prefixColors: args.prefixColors.split(','),
|
|
168
177
|
prefixLength: args.prefixLength,
|
|
169
178
|
restartDelay: args.restartAfter,
|
|
170
179
|
restartTries: args.restartTries,
|
|
171
180
|
successCondition: args.success,
|
|
172
|
-
timestampFormat: args.timestampFormat
|
|
181
|
+
timestampFormat: args.timestampFormat,
|
|
182
|
+
timings: args.timings
|
|
173
183
|
}).then(
|
|
174
184
|
() => process.exit(0),
|
|
175
185
|
() => process.exit(1)
|
package/bin/concurrently.spec.js
CHANGED
|
@@ -163,6 +163,26 @@ describe('--raw', () => {
|
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
describe('--hide', () => {
|
|
167
|
+
it('hides the output of a process by its index', done => {
|
|
168
|
+
const child = run('--hide 1 "echo foo" "echo bar"');
|
|
169
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
170
|
+
expect(lines).toContainEqual(expect.stringContaining('foo'));
|
|
171
|
+
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
|
|
172
|
+
done();
|
|
173
|
+
}, done);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('hides the output of a process by its name', done => {
|
|
177
|
+
const child = run('-n foo,bar --hide bar "echo foo" "echo bar"');
|
|
178
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
179
|
+
expect(lines).toContainEqual(expect.stringContaining('foo'));
|
|
180
|
+
expect(lines).not.toContainEqual(expect.stringContaining('bar'));
|
|
181
|
+
done();
|
|
182
|
+
}, done);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
166
186
|
describe('--names', () => {
|
|
167
187
|
it('is aliased to -n', done => {
|
|
168
188
|
const child = run('-n foo,bar "echo foo" "echo bar"');
|
|
@@ -326,7 +346,6 @@ describe('--handle-input', () => {
|
|
|
326
346
|
}, done);
|
|
327
347
|
});
|
|
328
348
|
|
|
329
|
-
|
|
330
349
|
it('forwards input to process --default-input-target', done => {
|
|
331
350
|
const lines = [];
|
|
332
351
|
const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
|
|
@@ -363,3 +382,47 @@ describe('--handle-input', () => {
|
|
|
363
382
|
}, done);
|
|
364
383
|
});
|
|
365
384
|
});
|
|
385
|
+
|
|
386
|
+
describe('--timings', () => {
|
|
387
|
+
const defaultTimestampFormatRegex = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/;
|
|
388
|
+
const processStartedMessageRegex = (index, command) => {
|
|
389
|
+
return new RegExp( `^\\[${ index }] ${ command } started at ${ defaultTimestampFormatRegex.source }$` );
|
|
390
|
+
};
|
|
391
|
+
const processStoppedMessageRegex = (index, command) => {
|
|
392
|
+
return new RegExp( `^\\[${ index }] ${ command } stopped at ${ defaultTimestampFormatRegex.source } after (\\d|,)+ms$` );
|
|
393
|
+
};
|
|
394
|
+
const expectLinesForProcessStartAndStop = (lines, index, command) => {
|
|
395
|
+
const escapedCommand = _.escapeRegExp(command);
|
|
396
|
+
expect(lines).toContainEqual(expect.stringMatching(processStartedMessageRegex(index, escapedCommand)));
|
|
397
|
+
expect(lines).toContainEqual(expect.stringMatching(processStoppedMessageRegex(index, escapedCommand)));
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const expectLinesForTimingsTable = (lines) => {
|
|
401
|
+
const tableTopBorderRegex = /┌[─┬]+┐/g;
|
|
402
|
+
expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));
|
|
403
|
+
const tableHeaderRowRegex = /(\W+(name|duration|exit code|killed|command)\W+){5}/g;
|
|
404
|
+
expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));
|
|
405
|
+
const tableBottomBorderRegex = /└[─┴]+┘/g;
|
|
406
|
+
expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
it('shows timings on success', done => {
|
|
410
|
+
const child = run('--timings "sleep 0.5" "exit 0"');
|
|
411
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
412
|
+
expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.5');
|
|
413
|
+
expectLinesForProcessStartAndStop(lines, 1, 'exit 0');
|
|
414
|
+
expectLinesForTimingsTable(lines);
|
|
415
|
+
done();
|
|
416
|
+
}, done);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('shows timings on failure', done => {
|
|
420
|
+
const child = run('--timings "sleep 0.75" "exit 1"');
|
|
421
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
422
|
+
expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.75');
|
|
423
|
+
expectLinesForProcessStartAndStop(lines, 1, 'exit 1');
|
|
424
|
+
expectLinesForTimingsTable(lines);
|
|
425
|
+
done();
|
|
426
|
+
}, done);
|
|
427
|
+
});
|
|
428
|
+
});
|
package/bin/epilogue.txt
CHANGED
|
@@ -16,6 +16,10 @@ Examples:
|
|
|
16
16
|
|
|
17
17
|
$ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"
|
|
18
18
|
|
|
19
|
+
- Configuring via environment variables with CONCURRENTLY_ prefix
|
|
20
|
+
|
|
21
|
+
$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true $0 "echo hello" "echo world"
|
|
22
|
+
|
|
19
23
|
- Send input to default
|
|
20
24
|
|
|
21
25
|
$ $0 --handle-input "nodemon" "npm run watch-js"
|
|
@@ -35,8 +39,8 @@ Examples:
|
|
|
35
39
|
|
|
36
40
|
$ $0 npm:watch-node npm:watch-js npm:watch-css
|
|
37
41
|
|
|
38
|
-
- Shortened NPM run command with wildcard
|
|
42
|
+
- Shortened NPM run command with wildcard (make sure to wrap it in quotes!)
|
|
39
43
|
|
|
40
|
-
$ $0 npm:watch-*
|
|
44
|
+
$ $0 "npm:watch-*"
|
|
41
45
|
|
|
42
46
|
For more details, visit https://github.com/open-cli-tools/concurrently
|
package/index.js
CHANGED
|
@@ -8,9 +8,11 @@ const RestartProcess = require('./src/flow-control/restart-process');
|
|
|
8
8
|
|
|
9
9
|
const concurrently = require('./src/concurrently');
|
|
10
10
|
const Logger = require('./src/logger');
|
|
11
|
+
const LogTimings = require( './src/flow-control/log-timings' );
|
|
11
12
|
|
|
12
13
|
module.exports = exports = (commands, options = {}) => {
|
|
13
14
|
const logger = new Logger({
|
|
15
|
+
hide: options.hide,
|
|
14
16
|
outputStream: options.outputStream || process.stdout,
|
|
15
17
|
prefixFormat: options.prefix,
|
|
16
18
|
prefixLength: options.prefixLength,
|
|
@@ -42,8 +44,14 @@ module.exports = exports = (commands, options = {}) => {
|
|
|
42
44
|
new KillOthers({
|
|
43
45
|
logger,
|
|
44
46
|
conditions: options.killOthers
|
|
47
|
+
}),
|
|
48
|
+
new LogTimings({
|
|
49
|
+
logger: options.timings ? logger: null,
|
|
50
|
+
timestampFormat: options.timestampFormat,
|
|
45
51
|
})
|
|
46
|
-
]
|
|
52
|
+
],
|
|
53
|
+
prefixColors: options.prefixColors || [],
|
|
54
|
+
timings: options.timings
|
|
47
55
|
});
|
|
48
56
|
};
|
|
49
57
|
|
|
@@ -58,3 +66,4 @@ exports.LogError = LogError;
|
|
|
58
66
|
exports.LogExit = LogExit;
|
|
59
67
|
exports.LogOutput = LogOutput;
|
|
60
68
|
exports.RestartProcess = RestartProcess;
|
|
69
|
+
exports.LogTimings = LogTimings;
|
package/package.json
CHANGED
|
@@ -32,12 +32,21 @@ module.exports = class ExpandNpmWildcard {
|
|
|
32
32
|
const preWildcard = _.escapeRegExp(cmdName.substr(0, wildcardPosition));
|
|
33
33
|
const postWildcard = _.escapeRegExp(cmdName.substr(wildcardPosition + 1));
|
|
34
34
|
const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);
|
|
35
|
+
const currentName = commandInfo.name || '';
|
|
35
36
|
|
|
36
37
|
return this.scripts
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
.map(script => {
|
|
39
|
+
const match = script.match(wildcardRegex);
|
|
40
|
+
|
|
41
|
+
if (match) {
|
|
42
|
+
return Object.assign({}, commandInfo, {
|
|
43
|
+
command: `${npmCmd} run ${script}${args}`,
|
|
44
|
+
// Will use an empty command name if command has no name and the wildcard match is empty,
|
|
45
|
+
// e.g. if `npm:watch-*` matches `npm run watch-`.
|
|
46
|
+
name: currentName + match[1],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean);
|
|
42
51
|
}
|
|
43
52
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ExpandNpmWildcard = require('./expand-npm-wildcard');
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
|
|
3
4
|
let parser, readPkg;
|
|
4
5
|
|
|
@@ -7,6 +8,34 @@ beforeEach(() => {
|
|
|
7
8
|
parser = new ExpandNpmWildcard(readPkg);
|
|
8
9
|
});
|
|
9
10
|
|
|
11
|
+
describe('ExpandNpmWildcard#readPackage', () => {
|
|
12
|
+
it('can read package', () => {
|
|
13
|
+
const expectedPackage = {
|
|
14
|
+
'name': 'concurrently',
|
|
15
|
+
'version': '6.4.0',
|
|
16
|
+
};
|
|
17
|
+
jest.spyOn(fs, 'readFileSync').mockImplementation((path, options) => {
|
|
18
|
+
if (path === 'package.json') {
|
|
19
|
+
return JSON.stringify(expectedPackage);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const actualReadPackage = ExpandNpmWildcard.readPackage();
|
|
25
|
+
expect(actualReadPackage).toEqual(expectedPackage);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('can handle errors reading package', () => {
|
|
29
|
+
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
30
|
+
throw new Error('Error reading package');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(() => ExpandNpmWildcard.readPackage()).not.toThrow();
|
|
34
|
+
expect(ExpandNpmWildcard.readPackage()).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
});
|
|
38
|
+
|
|
10
39
|
it('returns same command if not an npm run command', () => {
|
|
11
40
|
const commandInfo = {
|
|
12
41
|
command: 'npm test'
|
|
@@ -42,8 +71,25 @@ for (const npmCmd of ['npm', 'yarn', 'pnpm']) {
|
|
|
42
71
|
});
|
|
43
72
|
|
|
44
73
|
expect(parser.parse({ command: `${npmCmd} run foo-*-baz qux` })).toEqual([
|
|
45
|
-
{ name: '
|
|
46
|
-
{ name: '
|
|
74
|
+
{ name: 'bar', command: `${npmCmd} run foo-bar-baz qux` },
|
|
75
|
+
{ name: '', command: `${npmCmd} run foo--baz qux` },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('uses existing command name as prefix to the wildcard match', () => {
|
|
80
|
+
readPkg.mockReturnValue({
|
|
81
|
+
scripts: {
|
|
82
|
+
'watch-js': '',
|
|
83
|
+
'watch-css': '',
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(parser.parse({
|
|
88
|
+
name: 'w:',
|
|
89
|
+
command: `${npmCmd} run watch-*`,
|
|
90
|
+
})).toEqual([
|
|
91
|
+
{ name: 'w:js', command: `${npmCmd} run watch-js` },
|
|
92
|
+
{ name: 'w:css', command: `${npmCmd} run watch-css` },
|
|
47
93
|
]);
|
|
48
94
|
});
|
|
49
95
|
|
package/src/command.js
CHANGED
|
@@ -17,6 +17,7 @@ module.exports = class Command {
|
|
|
17
17
|
this.killed = false;
|
|
18
18
|
|
|
19
19
|
this.error = new Rx.Subject();
|
|
20
|
+
this.timer = new Rx.Subject();
|
|
20
21
|
this.close = new Rx.Subject();
|
|
21
22
|
this.stdout = new Rx.Subject();
|
|
22
23
|
this.stderr = new Rx.Subject();
|
|
@@ -26,13 +27,21 @@ module.exports = class Command {
|
|
|
26
27
|
const child = this.spawn(this.command, this.spawnOpts);
|
|
27
28
|
this.process = child;
|
|
28
29
|
this.pid = child.pid;
|
|
30
|
+
const startDate = new Date(Date.now());
|
|
31
|
+
const highResStartTime = process.hrtime();
|
|
32
|
+
this.timer.next({startDate});
|
|
29
33
|
|
|
30
34
|
Rx.fromEvent(child, 'error').subscribe(event => {
|
|
31
35
|
this.process = undefined;
|
|
36
|
+
const endDate = new Date(Date.now());
|
|
37
|
+
this.timer.next({startDate, endDate});
|
|
32
38
|
this.error.next(event);
|
|
33
39
|
});
|
|
34
40
|
Rx.fromEvent(child, 'close').subscribe(([exitCode, signal]) => {
|
|
35
41
|
this.process = undefined;
|
|
42
|
+
const endDate = new Date(Date.now());
|
|
43
|
+
this.timer.next({startDate, endDate});
|
|
44
|
+
const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);
|
|
36
45
|
this.close.next({
|
|
37
46
|
command: {
|
|
38
47
|
command: this.command,
|
|
@@ -43,6 +52,11 @@ module.exports = class Command {
|
|
|
43
52
|
index: this.index,
|
|
44
53
|
exitCode: exitCode === null ? signal : exitCode,
|
|
45
54
|
killed: this.killed,
|
|
55
|
+
timings: {
|
|
56
|
+
startDate,
|
|
57
|
+
endDate,
|
|
58
|
+
durationSeconds: durationSeconds + (durationNanoSeconds / 1e9),
|
|
59
|
+
}
|
|
46
60
|
});
|
|
47
61
|
});
|
|
48
62
|
child.stdout && pipeTo(Rx.fromEvent(child.stdout, 'data'), this.stdout);
|
package/src/command.spec.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
|
+
const process = require('process');
|
|
2
3
|
const Command = require('./command');
|
|
3
4
|
|
|
4
5
|
const createProcess = () => {
|
|
@@ -54,6 +55,72 @@ describe('#start()', () => {
|
|
|
54
55
|
process.emit('error', 'foo');
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it('shares start and close timing events to the timing stream', done => {
|
|
59
|
+
const process = createProcess();
|
|
60
|
+
const command = new Command({ spawn: () => process });
|
|
61
|
+
|
|
62
|
+
const startDate = new Date();
|
|
63
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
64
|
+
jest.spyOn(Date, 'now')
|
|
65
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
66
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
67
|
+
|
|
68
|
+
let callCount = 0;
|
|
69
|
+
command.timer.subscribe(({startDate: actualStartDate, endDate: actualEndDate}) => {
|
|
70
|
+
switch (callCount) {
|
|
71
|
+
case 0:
|
|
72
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
73
|
+
expect(actualEndDate).toBeUndefined();
|
|
74
|
+
break;
|
|
75
|
+
case 1:
|
|
76
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
77
|
+
expect(actualEndDate).toStrictEqual(endDate);
|
|
78
|
+
done();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
throw new Error('Unexpected call count');
|
|
82
|
+
}
|
|
83
|
+
callCount++;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
command.start();
|
|
87
|
+
process.emit('close', 0, null);
|
|
88
|
+
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('shares start and error timing events to the timing stream', done => {
|
|
92
|
+
const process = createProcess();
|
|
93
|
+
const command = new Command({ spawn: () => process });
|
|
94
|
+
|
|
95
|
+
const startDate = new Date();
|
|
96
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
97
|
+
jest.spyOn(Date, 'now')
|
|
98
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
99
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
100
|
+
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
command.timer.subscribe(({startDate: actualStartDate, endDate: actualEndDate}) => {
|
|
103
|
+
switch (callCount) {
|
|
104
|
+
case 0:
|
|
105
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
106
|
+
expect(actualEndDate).toBeUndefined();
|
|
107
|
+
break;
|
|
108
|
+
case 1:
|
|
109
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
110
|
+
expect(actualEndDate).toStrictEqual(endDate);
|
|
111
|
+
done();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
throw new Error('Unexpected call count');
|
|
115
|
+
}
|
|
116
|
+
callCount++;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
command.start();
|
|
120
|
+
process.emit('error', 0, null);
|
|
121
|
+
|
|
122
|
+
});
|
|
123
|
+
|
|
57
124
|
it('shares closes to the close stream with exit code', done => {
|
|
58
125
|
const process = createProcess();
|
|
59
126
|
const command = new Command({ spawn: () => process });
|
|
@@ -83,6 +150,31 @@ describe('#start()', () => {
|
|
|
83
150
|
process.emit('close', null, 'SIGKILL');
|
|
84
151
|
});
|
|
85
152
|
|
|
153
|
+
it('shares closes to the close stream with timing information', done => {
|
|
154
|
+
const process1 = createProcess();
|
|
155
|
+
const command = new Command({ spawn: () => process1 });
|
|
156
|
+
|
|
157
|
+
const startDate = new Date();
|
|
158
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
159
|
+
jest.spyOn(Date, 'now')
|
|
160
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
161
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
162
|
+
|
|
163
|
+
jest.spyOn(process, 'hrtime')
|
|
164
|
+
.mockReturnValueOnce([0, 0])
|
|
165
|
+
.mockReturnValueOnce([1, 1e8]);
|
|
166
|
+
|
|
167
|
+
command.close.subscribe(data => {
|
|
168
|
+
expect(data.timings.startDate).toStrictEqual(startDate);
|
|
169
|
+
expect(data.timings.endDate).toStrictEqual(endDate);
|
|
170
|
+
expect(data.timings.durationSeconds).toBe(1.1);
|
|
171
|
+
done();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
command.start();
|
|
175
|
+
process1.emit('close', null, 'SIGKILL');
|
|
176
|
+
});
|
|
177
|
+
|
|
86
178
|
it('shares closes to the close stream with command info and index', done => {
|
|
87
179
|
const process = createProcess();
|
|
88
180
|
const commandInfo = {
|
|
@@ -170,7 +262,7 @@ describe('#kill()', () => {
|
|
|
170
262
|
|
|
171
263
|
it('marks the command as killed', done => {
|
|
172
264
|
command.start();
|
|
173
|
-
|
|
265
|
+
|
|
174
266
|
command.close.subscribe(data => {
|
|
175
267
|
expect(data.exitCode).toBe(1);
|
|
176
268
|
expect(data.killed).toBe(true);
|
package/src/concurrently.js
CHANGED
|
@@ -18,6 +18,7 @@ const defaults = {
|
|
|
18
18
|
raw: false,
|
|
19
19
|
controllers: [],
|
|
20
20
|
cwd: undefined,
|
|
21
|
+
timings: false
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
module.exports = (commands, options) => {
|
|
@@ -32,21 +33,28 @@ module.exports = (commands, options) => {
|
|
|
32
33
|
new ExpandNpmWildcard()
|
|
33
34
|
];
|
|
34
35
|
|
|
36
|
+
let lastColor = '';
|
|
35
37
|
commands = _(commands)
|
|
36
38
|
.map(mapToCommandInfo)
|
|
37
39
|
.flatMap(command => parseCommand(command, commandParsers))
|
|
38
|
-
.map((command, index) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
.map((command, index) => {
|
|
41
|
+
// Use documented behaviour of repeating last color when specifying more commands than colors
|
|
42
|
+
lastColor = options.prefixColors && options.prefixColors[index] || lastColor;
|
|
43
|
+
return new Command(
|
|
44
|
+
Object.assign({
|
|
45
|
+
index,
|
|
46
|
+
spawnOpts: getSpawnOpts({
|
|
47
|
+
raw: options.raw,
|
|
48
|
+
env: command.env,
|
|
49
|
+
cwd: command.cwd || options.cwd,
|
|
50
|
+
}),
|
|
51
|
+
prefixColor: lastColor,
|
|
52
|
+
killProcess: options.kill,
|
|
53
|
+
spawn: options.spawn,
|
|
54
|
+
timings: options.timings,
|
|
55
|
+
}, command)
|
|
56
|
+
);
|
|
57
|
+
})
|
|
50
58
|
.value();
|
|
51
59
|
|
|
52
60
|
const handleResult = options.controllers.reduce(
|
|
@@ -67,7 +75,9 @@ module.exports = (commands, options) => {
|
|
|
67
75
|
maybeRunMore(commandsLeft);
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
return new CompletionListener({
|
|
78
|
+
return new CompletionListener({
|
|
79
|
+
successCondition: options.successCondition,
|
|
80
|
+
})
|
|
71
81
|
.listen(commands)
|
|
72
82
|
.finally(() => {
|
|
73
83
|
handleResult.onFinishCallbacks.forEach((onFinish) => onFinish());
|
|
@@ -75,13 +85,15 @@ module.exports = (commands, options) => {
|
|
|
75
85
|
};
|
|
76
86
|
|
|
77
87
|
function mapToCommandInfo(command) {
|
|
78
|
-
return {
|
|
88
|
+
return Object.assign({
|
|
79
89
|
command: command.command || command,
|
|
80
90
|
name: command.name || '',
|
|
81
|
-
prefixColor: command.prefixColor || '',
|
|
82
91
|
env: command.env || {},
|
|
83
92
|
cwd: command.cwd || '',
|
|
84
|
-
|
|
93
|
+
|
|
94
|
+
}, command.prefixColor ? {
|
|
95
|
+
prefixColor: command.prefixColor,
|
|
96
|
+
} : {});
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
function parseCommand(command, parsers) {
|
package/src/concurrently.spec.js
CHANGED
|
@@ -84,6 +84,19 @@ it('runs commands with a name or prefix color', () => {
|
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
+
it('runs commands with a list of colors', () => {
|
|
88
|
+
create(['echo', 'kill'], {
|
|
89
|
+
prefixColors: ['red']
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
controllers.forEach(controller => {
|
|
93
|
+
expect(controller.handle).toHaveBeenCalledWith([
|
|
94
|
+
expect.objectContaining({ command: 'echo', prefixColor: 'red' }),
|
|
95
|
+
expect.objectContaining({ command: 'kill', prefixColor: 'red' }),
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
87
100
|
it('passes commands wrapped from a controller to the next one', () => {
|
|
88
101
|
const fakeCommand = createFakeCommand('banana', 'banana');
|
|
89
102
|
controllers[0].handle.mockReturnValue({ commands: [fakeCommand] });
|
package/src/defaults.js
CHANGED
|
@@ -10,6 +10,8 @@ module.exports = {
|
|
|
10
10
|
handleInput: false,
|
|
11
11
|
// How many processes to run at once
|
|
12
12
|
maxProcesses: 0,
|
|
13
|
+
// Indices and names of commands whose output to be not logged
|
|
14
|
+
hide: '',
|
|
13
15
|
nameSeparator: ',',
|
|
14
16
|
// Which prefix style to use when logging processes output.
|
|
15
17
|
prefix: '',
|
|
@@ -27,5 +29,7 @@ module.exports = {
|
|
|
27
29
|
// Refer to https://date-fns.org/v2.0.1/docs/format
|
|
28
30
|
timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS',
|
|
29
31
|
// Current working dir passed as option to spawn command. Default: process.cwd()
|
|
30
|
-
cwd: undefined
|
|
32
|
+
cwd: undefined,
|
|
33
|
+
// Whether to show timing information for processes in console output
|
|
34
|
+
timings: false,
|
|
31
35
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const stream = require('stream');
|
|
2
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
3
|
+
|
|
4
|
+
const Logger = require('../logger');
|
|
5
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
6
|
+
const BaseHandler = require('./base-handler');
|
|
7
|
+
|
|
8
|
+
let commands, controller, inputStream, logger;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
commands = [
|
|
12
|
+
createFakeCommand('foo', 'echo foo', 0),
|
|
13
|
+
createFakeCommand('bar', 'echo bar', 1),
|
|
14
|
+
];
|
|
15
|
+
inputStream = new stream.PassThrough();
|
|
16
|
+
logger = createMockInstance(Logger);
|
|
17
|
+
controller = new BaseHandler({ logger });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns same commands and null onFinish callback by default', () => {
|
|
21
|
+
expect(controller.handle(commands)).toMatchObject({ commands, onFinish: null });
|
|
22
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const formatDate = require('date-fns/format');
|
|
2
|
+
const Rx = require('rxjs');
|
|
3
|
+
const { bufferCount, take } = require('rxjs/operators');
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const BaseHandler = require('./base-handler');
|
|
6
|
+
|
|
7
|
+
module.exports = class LogTimings extends BaseHandler {
|
|
8
|
+
constructor({ logger, timestampFormat }) {
|
|
9
|
+
super({ logger });
|
|
10
|
+
|
|
11
|
+
this.timestampFormat = timestampFormat;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
printExitInfoTimingTable(exitInfos) {
|
|
15
|
+
const exitInfoTable = _(exitInfos)
|
|
16
|
+
.sortBy(({ timings }) => timings.durationSeconds)
|
|
17
|
+
.reverse()
|
|
18
|
+
.map(({ command, timings, killed, exitCode }) => {
|
|
19
|
+
const readableDurationMs = (timings.endDate - timings.startDate).toLocaleString();
|
|
20
|
+
return {
|
|
21
|
+
name: command.name,
|
|
22
|
+
duration: `${readableDurationMs}ms`,
|
|
23
|
+
'exit code': exitCode,
|
|
24
|
+
killed,
|
|
25
|
+
command: command.command,
|
|
26
|
+
};
|
|
27
|
+
})
|
|
28
|
+
.value();
|
|
29
|
+
|
|
30
|
+
this.logger.logGlobalEvent('Timings:');
|
|
31
|
+
this.logger.logTable(exitInfoTable);
|
|
32
|
+
return exitInfos;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handle(commands) {
|
|
36
|
+
if (!this.logger) {
|
|
37
|
+
return { commands };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// individual process timings
|
|
41
|
+
commands.forEach(command => {
|
|
42
|
+
command.timer.subscribe(({ startDate, endDate }) => {
|
|
43
|
+
if (!endDate) {
|
|
44
|
+
const formattedStartDate = formatDate(startDate, this.timestampFormat);
|
|
45
|
+
this.logger.logCommandEvent(`${command.command} started at ${formattedStartDate}`, command);
|
|
46
|
+
} else {
|
|
47
|
+
const durationMs = endDate.getTime() - startDate.getTime();
|
|
48
|
+
const formattedEndDate = formatDate(endDate, this.timestampFormat);
|
|
49
|
+
this.logger.logCommandEvent(`${command.command} stopped at ${formattedEndDate} after ${durationMs.toLocaleString()}ms`, command);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// overall summary timings
|
|
55
|
+
const closeStreams = commands.map(command => command.close);
|
|
56
|
+
this.allProcessesClosed = Rx.merge(...closeStreams).pipe(
|
|
57
|
+
bufferCount(closeStreams.length),
|
|
58
|
+
take(1),
|
|
59
|
+
);
|
|
60
|
+
this.allProcessesClosed.subscribe((exitInfos) => this.printExitInfoTimingTable(exitInfos));
|
|
61
|
+
|
|
62
|
+
return { commands };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const formatDate = require('date-fns/format');
|
|
3
|
+
const Logger = require('../logger');
|
|
4
|
+
const LogTimings = require( './log-timings' );
|
|
5
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
6
|
+
|
|
7
|
+
// shown in timing order
|
|
8
|
+
const startDate0 = new Date();
|
|
9
|
+
const startDate1 = new Date(startDate0.getTime() + 1000);
|
|
10
|
+
const endDate1 = new Date(startDate0.getTime() + 3000);
|
|
11
|
+
const endDate0 = new Date(startDate0.getTime() + 5000);
|
|
12
|
+
|
|
13
|
+
const timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS';
|
|
14
|
+
const getDurationText = (startDate, endDate) => `${(endDate.getTime() - startDate.getTime()).toLocaleString()}ms`;
|
|
15
|
+
const command0DurationTextMs = getDurationText(startDate0, endDate0);
|
|
16
|
+
const command1DurationTextMs = getDurationText(startDate1, endDate1);
|
|
17
|
+
|
|
18
|
+
const exitInfoToTimingInfo = ({ command, timings, killed, exitCode }) => {
|
|
19
|
+
const readableDurationMs = getDurationText(timings.startDate, timings.endDate);
|
|
20
|
+
return {
|
|
21
|
+
name: command.name,
|
|
22
|
+
duration: readableDurationMs,
|
|
23
|
+
'exit code': exitCode,
|
|
24
|
+
killed,
|
|
25
|
+
command: command.command,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let controller, logger, commands, command0ExitInfo, command1ExitInfo;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
commands = [
|
|
33
|
+
createFakeCommand('foo', 'command 1', 0),
|
|
34
|
+
createFakeCommand('bar', 'command 2', 1),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
command0ExitInfo = {
|
|
38
|
+
command: commands[0].command,
|
|
39
|
+
timings: {
|
|
40
|
+
startDate: startDate0,
|
|
41
|
+
endDate: endDate0,
|
|
42
|
+
},
|
|
43
|
+
index: commands[0].index,
|
|
44
|
+
killed: false,
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
command1ExitInfo = {
|
|
49
|
+
command: commands[1].command,
|
|
50
|
+
timings: {
|
|
51
|
+
startDate: startDate1,
|
|
52
|
+
endDate: endDate1,
|
|
53
|
+
},
|
|
54
|
+
index: commands[1].index,
|
|
55
|
+
killed: false,
|
|
56
|
+
exitCode: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
logger = createMockInstance(Logger);
|
|
60
|
+
controller = new LogTimings({ logger, timestampFormat });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns same commands', () => {
|
|
64
|
+
expect(controller.handle(commands)).toMatchObject({ commands });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not log timings and doesn\'t throw if no logger is provided', () => {
|
|
68
|
+
controller = new LogTimings({ });
|
|
69
|
+
controller.handle(commands);
|
|
70
|
+
|
|
71
|
+
commands[0].timer.next({ startDate: startDate0 });
|
|
72
|
+
commands[1].timer.next({ startDate: startDate1 });
|
|
73
|
+
commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });
|
|
74
|
+
commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });
|
|
75
|
+
|
|
76
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('logs the timings at the start and end (ie complete or error) event of each command', () => {
|
|
80
|
+
controller.handle(commands);
|
|
81
|
+
|
|
82
|
+
commands[0].timer.next({ startDate: startDate0 });
|
|
83
|
+
commands[1].timer.next({ startDate: startDate1 });
|
|
84
|
+
commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });
|
|
85
|
+
commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });
|
|
86
|
+
|
|
87
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(4);
|
|
88
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
89
|
+
`${commands[0].command} started at ${formatDate(startDate0, timestampFormat)}`,
|
|
90
|
+
commands[0]
|
|
91
|
+
);
|
|
92
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
93
|
+
`${commands[1].command} started at ${formatDate(startDate1, timestampFormat)}`,
|
|
94
|
+
commands[1]
|
|
95
|
+
);
|
|
96
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
97
|
+
`${commands[1].command} stopped at ${formatDate(endDate1, timestampFormat)} after ${command1DurationTextMs}`,
|
|
98
|
+
commands[1]
|
|
99
|
+
);
|
|
100
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
101
|
+
`${commands[0].command} stopped at ${formatDate(endDate0, timestampFormat)} after ${command0DurationTextMs}`,
|
|
102
|
+
commands[0]
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not log timings summary if there was an error', () => {
|
|
107
|
+
controller.handle(commands);
|
|
108
|
+
|
|
109
|
+
commands[0].close.next(command0ExitInfo);
|
|
110
|
+
commands[1].error.next();
|
|
111
|
+
|
|
112
|
+
expect(logger.logTable).toHaveBeenCalledTimes(0);
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('logs the sorted timings summary when all processes close successfully', () => {
|
|
117
|
+
jest.spyOn(controller, 'printExitInfoTimingTable');
|
|
118
|
+
controller.handle(commands);
|
|
119
|
+
|
|
120
|
+
commands[0].close.next(command0ExitInfo);
|
|
121
|
+
commands[1].close.next(command1ExitInfo);
|
|
122
|
+
|
|
123
|
+
expect(logger.logTable).toHaveBeenCalledTimes(1);
|
|
124
|
+
|
|
125
|
+
// un-sorted ie by finish order
|
|
126
|
+
expect(controller.printExitInfoTimingTable).toHaveBeenCalledWith([
|
|
127
|
+
command0ExitInfo,
|
|
128
|
+
command1ExitInfo
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// sorted by duration
|
|
132
|
+
expect(logger.logTable).toHaveBeenCalledWith([
|
|
133
|
+
exitInfoToTimingInfo(command1ExitInfo),
|
|
134
|
+
exitInfoToTimingInfo(command0ExitInfo)
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
});
|
|
@@ -69,7 +69,15 @@ it('restarts processes up to tries', () => {
|
|
|
69
69
|
expect(commands[0].start).toHaveBeenCalledTimes(2);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it
|
|
72
|
+
it('restart processes forever, if tries is negative', () => {
|
|
73
|
+
controller = new RestartProcess({
|
|
74
|
+
logger,
|
|
75
|
+
scheduler,
|
|
76
|
+
delay: 100,
|
|
77
|
+
tries: -1
|
|
78
|
+
});
|
|
79
|
+
expect(controller.tries).toBe(Infinity);
|
|
80
|
+
});
|
|
73
81
|
|
|
74
82
|
it('restarts processes until they succeed', () => {
|
|
75
83
|
controller.handle(commands);
|
package/src/logger.js
CHANGED
|
@@ -5,7 +5,11 @@ const formatDate = require('date-fns/format');
|
|
|
5
5
|
const defaults = require('./defaults');
|
|
6
6
|
|
|
7
7
|
module.exports = class Logger {
|
|
8
|
-
constructor({ outputStream, prefixFormat, prefixLength, raw, timestampFormat }) {
|
|
8
|
+
constructor({ hide, outputStream, prefixFormat, prefixLength, raw, timestampFormat }) {
|
|
9
|
+
// To avoid empty strings from hiding the output of commands that don't have a name,
|
|
10
|
+
// keep in the list of commands to hide only strings with some length.
|
|
11
|
+
// This might happen through the CLI when no `--hide` argument is specified, for example.
|
|
12
|
+
this.hide = _.castArray(hide).filter(name => name || name === 0).map(String);
|
|
9
13
|
this.raw = raw;
|
|
10
14
|
this.outputStream = outputStream;
|
|
11
15
|
this.prefixFormat = prefixFormat;
|
|
@@ -76,6 +80,10 @@ module.exports = class Logger {
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
logCommandText(text, command) {
|
|
83
|
+
if (this.hide.includes(String(command.index)) || this.hide.includes(command.name)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
const prefix = this.colorText(command, this.getPrefix(command));
|
|
80
88
|
return this.log(prefix + (prefix ? ' ' : ''), text);
|
|
81
89
|
}
|
|
@@ -88,6 +96,63 @@ module.exports = class Logger {
|
|
|
88
96
|
this.log(chalk.reset('-->') + ' ', chalk.reset(text) + '\n');
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
logTable(tableContents) {
|
|
100
|
+
// For now, can only print array tables with some content.
|
|
101
|
+
if (this.raw || !Array.isArray(tableContents) || !tableContents.length) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let nextColIndex = 0;
|
|
106
|
+
const headers = {};
|
|
107
|
+
const contentRows = tableContents.map(row => {
|
|
108
|
+
const rowContents = [];
|
|
109
|
+
Object.keys(row).forEach((col) => {
|
|
110
|
+
if (!headers[col]) {
|
|
111
|
+
headers[col] = {
|
|
112
|
+
index: nextColIndex++,
|
|
113
|
+
//
|
|
114
|
+
length: col.length,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const colIndex = headers[col].index;
|
|
119
|
+
const formattedValue = String(row[col] == null ? '' : row[col]);
|
|
120
|
+
// Update the column length in case this rows value is longer than the previous length for the column.
|
|
121
|
+
headers[col].length = Math.max(formattedValue.length, headers[col].length);
|
|
122
|
+
rowContents[colIndex] = formattedValue;
|
|
123
|
+
return rowContents;
|
|
124
|
+
});
|
|
125
|
+
return rowContents;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const headersFormatted = Object
|
|
129
|
+
.keys(headers)
|
|
130
|
+
.map(header => header.padEnd(headers[header].length, ' '));
|
|
131
|
+
|
|
132
|
+
if (!headersFormatted.length) {
|
|
133
|
+
// No columns exist.
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const borderRowFormatted = headersFormatted.map(header => '─'.padEnd(header.length, '─'));
|
|
138
|
+
|
|
139
|
+
this.logGlobalEvent(`┌─${borderRowFormatted.join('─┬─')}─┐`);
|
|
140
|
+
this.logGlobalEvent(`│ ${headersFormatted.join(' │ ')} │`);
|
|
141
|
+
this.logGlobalEvent(`├─${borderRowFormatted.join('─┼─')}─┤`);
|
|
142
|
+
|
|
143
|
+
contentRows.forEach(contentRow => {
|
|
144
|
+
const contentRowFormatted = headersFormatted.map((header, colIndex) => {
|
|
145
|
+
// If the table was expanded after this row was processed, it won't have this column.
|
|
146
|
+
// Use an empty string in this case.
|
|
147
|
+
const col = contentRow[colIndex] || '';
|
|
148
|
+
return col.padEnd(header.length, ' ');
|
|
149
|
+
});
|
|
150
|
+
this.logGlobalEvent(`│ ${contentRowFormatted.join(' │ ')} │`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.logGlobalEvent(`└─${borderRowFormatted.join('─┴─')}─┘`);
|
|
154
|
+
}
|
|
155
|
+
|
|
91
156
|
log(prefix, text) {
|
|
92
157
|
if (this.raw) {
|
|
93
158
|
return this.outputStream.write(text);
|
package/src/logger.spec.js
CHANGED
|
@@ -176,6 +176,20 @@ describe('#logCommandText()', () => {
|
|
|
176
176
|
|
|
177
177
|
expect(logger.log).toHaveBeenCalledWith(chalk.hex(prefixColor)('[1]') + ' ', 'foo');
|
|
178
178
|
});
|
|
179
|
+
|
|
180
|
+
it('does nothing if command is hidden by name', () => {
|
|
181
|
+
const logger = createLogger({ hide: ['abc'] });
|
|
182
|
+
logger.logCommandText('foo', { name: 'abc' });
|
|
183
|
+
|
|
184
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('does nothing if command is hidden by index', () => {
|
|
188
|
+
const logger = createLogger({ hide: [3] });
|
|
189
|
+
logger.logCommandText('foo', { index: 3 });
|
|
190
|
+
|
|
191
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
179
193
|
});
|
|
180
194
|
|
|
181
195
|
describe('#logCommandEvent()', () => {
|
|
@@ -186,6 +200,20 @@ describe('#logCommandEvent()', () => {
|
|
|
186
200
|
expect(logger.log).not.toHaveBeenCalled();
|
|
187
201
|
});
|
|
188
202
|
|
|
203
|
+
it('does nothing if command is hidden by name', () => {
|
|
204
|
+
const logger = createLogger({ hide: ['abc'] });
|
|
205
|
+
logger.logCommandEvent('foo', { name: 'abc' });
|
|
206
|
+
|
|
207
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('does nothing if command is hidden by index', () => {
|
|
211
|
+
const logger = createLogger({ hide: [3] });
|
|
212
|
+
logger.logCommandEvent('foo', { index: 3 });
|
|
213
|
+
|
|
214
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
|
|
189
217
|
it('logs text in gray dim', () => {
|
|
190
218
|
const logger = createLogger();
|
|
191
219
|
logger.logCommandEvent('foo', { index: 1 });
|
|
@@ -193,3 +221,98 @@ describe('#logCommandEvent()', () => {
|
|
|
193
221
|
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', chalk.reset('foo') + '\n');
|
|
194
222
|
});
|
|
195
223
|
});
|
|
224
|
+
|
|
225
|
+
describe('#logTable()', () => {
|
|
226
|
+
it('does not log anything in raw mode', () => {
|
|
227
|
+
const logger = createLogger({ raw: true });
|
|
228
|
+
logger.logTable([{ foo: 1, bar: 2 }]);
|
|
229
|
+
|
|
230
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('does not log anything if value is not an array', () => {
|
|
234
|
+
const logger = createLogger();
|
|
235
|
+
logger.logTable({});
|
|
236
|
+
logger.logTable(null);
|
|
237
|
+
logger.logTable(0);
|
|
238
|
+
logger.logTable('');
|
|
239
|
+
|
|
240
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does not log anything if array is empy', () => {
|
|
244
|
+
const logger = createLogger();
|
|
245
|
+
logger.logTable([]);
|
|
246
|
+
|
|
247
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('does not log anything if array items have no properties', () => {
|
|
251
|
+
const logger = createLogger();
|
|
252
|
+
logger.logTable([{}]);
|
|
253
|
+
|
|
254
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('logs a header for each item\'s properties', () => {
|
|
258
|
+
const logger = createLogger();
|
|
259
|
+
logger.logTable([{ foo: 1, bar: 2 }]);
|
|
260
|
+
|
|
261
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
262
|
+
chalk.reset('-->') + ' ',
|
|
263
|
+
chalk.reset('│ foo │ bar │') + '\n',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('logs padded headers according to longest column\'s value', () => {
|
|
268
|
+
const logger = createLogger();
|
|
269
|
+
logger.logTable([{ a: 'foo', b: 'barbaz' }]);
|
|
270
|
+
|
|
271
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
272
|
+
chalk.reset('-->') + ' ',
|
|
273
|
+
chalk.reset('│ a │ b │') + '\n',
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('logs each items\'s values', () => {
|
|
278
|
+
const logger = createLogger();
|
|
279
|
+
logger.logTable([{ foo: 123 }, { foo: 456 }]);
|
|
280
|
+
|
|
281
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
282
|
+
chalk.reset('-->') + ' ',
|
|
283
|
+
chalk.reset('│ 123 │') + '\n',
|
|
284
|
+
);
|
|
285
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
286
|
+
chalk.reset('-->') + ' ',
|
|
287
|
+
chalk.reset('│ 456 │') + '\n',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('logs each items\'s values padded according to longest column\'s value', () => {
|
|
292
|
+
const logger = createLogger();
|
|
293
|
+
logger.logTable([{ foo: 1 }, { foo: 123 }]);
|
|
294
|
+
|
|
295
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
296
|
+
chalk.reset('-->') + ' ',
|
|
297
|
+
chalk.reset('│ 1 │') + '\n',
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('logs items with different properties in each', () => {
|
|
302
|
+
const logger = createLogger();
|
|
303
|
+
logger.logTable([{ foo: 1 }, { bar: 2 }]);
|
|
304
|
+
|
|
305
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
306
|
+
chalk.reset('-->') + ' ',
|
|
307
|
+
chalk.reset('│ foo │ bar │') + '\n',
|
|
308
|
+
);
|
|
309
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
310
|
+
chalk.reset('-->') + ' ',
|
|
311
|
+
chalk.reset('│ 1 │ │') + '\n',
|
|
312
|
+
);
|
|
313
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
314
|
+
chalk.reset('-->') + ' ',
|
|
315
|
+
chalk.reset('│ │ 2 │') + '\n',
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
});
|