concurrently 6.2.0 → 6.4.0

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # Concurrently
2
2
 
3
- [![Travis Build Status](https://travis-ci.org/kimmobrunfeldt/concurrently.svg)](https://travis-ci.org/kimmobrunfeldt/concurrently) [![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/kimmobrunfeldt/concurrently?branch=master&svg=true)](https://ci.appveyor.com/project/kimmobrunfeldt/concurrently) *master branch status*
3
+ [![Build Status](https://github.com/open-cli-tools/concurrently/workflows/Tests/badge.svg)](https://github.com/open-cli-tools/concurrently/actions?workflow=Tests)
4
+ [![Coverage Status](https://coveralls.io/repos/github/open-cli-tools/concurrently/badge.svg?branch=master)](https://coveralls.io/github/open-cli-tools/concurrently?branch=master)
4
5
 
5
6
  [![NPM Badge](https://nodei.co/npm/concurrently.png?downloads=true)](https://www.npmjs.com/package/concurrently)
6
7
 
@@ -20,7 +21,7 @@ Like `npm run watch-js & npm run watch-less` but better.
20
21
 
21
22
  ## Why
22
23
 
23
- I like [task automation with npm](http://substack.net/task_automation_with_npm_run)
24
+ I like [task automation with npm](https://github.com/substack/blog/blob/master/npm_run.markdown)
24
25
  but the usual way to run multiple commands concurrently is
25
26
  `npm run watch-js & npm run watch-css`. That's fine but it's hard to keep
26
27
  on track of different outputs. Also if one process fails, others still keep running
@@ -115,21 +116,25 @@ Help:
115
116
  concurrently [options] <command ...>
116
117
 
117
118
  General
118
- -m, --max-processes How many processes should run at once.
119
- New processes only spawn after all restart tries of a
120
- process. [number]
121
- -n, --names List of custom names to be used in prefix template.
122
- Example names: "main,browser,server" [string]
123
- --name-separator The character to split <names> on. Example usage:
124
- concurrently -n "styles|scripts|server" --name-separator
125
- "|" [default: ","]
126
- -r, --raw Output only raw output of processes, disables prettifying
127
- and concurrently coloring. [boolean]
128
- -s, --success Return exit code of zero or one based on the success or
129
- failure of the "first" child to terminate, the "last
130
- child", or succeed only if "all" child processes succeed.
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.
131
133
  [choices: "first", "last", "all"] [default: "all"]
132
- --no-color Disables colors from logging [boolean]
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: ""]
133
138
 
134
139
  Prefix styling
135
140
  -p, --prefix Prefix used in logging for each process.
@@ -221,22 +226,26 @@ Examples:
221
226
 
222
227
  $ concurrently npm:watch-*
223
228
 
224
- For more details, visit https://github.com/kimmobrunfeldt/concurrently
229
+ For more details, visit https://github.com/open-cli-tools/concurrently
225
230
  ```
226
231
 
227
232
  ## Programmatic Usage
228
233
  concurrently can be used programmatically by using the API documented below:
229
234
 
230
235
  ### `concurrently(commands[, options])`
236
+
231
237
  - `commands`: an array of either strings (containing the commands to run) or objects
232
238
  with the shape `{ command, name, prefixColor, env, cwd }`.
239
+
233
240
  - `options` (optional): an object containing any of the below:
234
241
  - `cwd`: the working directory to be used by all commands. Can be overriden per command.
235
242
  Default: `process.cwd()`.
236
243
  - `defaultInputTarget`: the default input target when reading from `inputStream`.
237
244
  Default: `0`.
245
+ - `handleInput`: when `true`, reads input from `process.stdin`.
238
246
  - `inputStream`: a [`Readable` stream](https://nodejs.org/dist/latest-v10.x/docs/api/stream.html#stream_readable_streams)
239
- to read the input from, eg `process.stdin`.
247
+ to read the input from. Should only be used in the rare instance you would like to stream anything other than `process.stdin`. Overrides `handleInput`.
248
+ - `pauseInputStreamOnFinish`: by default, pauses the input stream (`process.stdin` when `handleInput` is enabled, or `inputStream` if provided) when all of the processes have finished. If you need to read from the input stream after `concurrently` has finished, set this to `false`. ([#252](https://github.com/kimmobrunfeldt/concurrently/issues/252)).
240
249
  - `killOthers`: an array of exitting conditions that will cause a process to kill others.
241
250
  Can contain any of `success` or `failure`.
242
251
  - `maxProcesses`: how many processes should run at once.
@@ -245,6 +254,9 @@ concurrently can be used programmatically by using the API documented below:
245
254
  - `prefix`: the prefix type to use when logging processes output.
246
255
  Possible values: `index`, `pid`, `time`, `command`, `name`, `none`, or a template (eg `[{time} process: {pid}]`).
247
256
  Default: the name of the process, or its index if no name is set.
257
+ - `prefixColors`: a list of colors as supported by [chalk](https://www.npmjs.com/package/chalk).
258
+ If concurrently would run more commands than there are colors, the last color is repeated.
259
+ Prefix colors specified per-command take precedence over this list.
248
260
  - `prefixLength`: how many characters to show when prefixing with `command`. Default: `10`
249
261
  - `raw`: whether raw mode should be used, meaning strictly process output will
250
262
  be logged, without any prefixes, colouring or extra stuff.
@@ -259,7 +271,7 @@ concurrently can be used programmatically by using the API documented below:
259
271
  > Returns: a `Promise` that resolves if the run was successful (according to `successCondition` option),
260
272
  > or rejects, containing an array of objects with information for each command that has been run, in the order
261
273
  > that the commands terminated. The objects have the shape `{ command, index, exitCode, killed }`, where `command` is the object
262
- > passed in the `commands` array, `index` its index there and `killed` indicates if the process was killed as a result of
274
+ > passed in the `commands` array, `index` its index there and `killed` indicates if the process was killed as a result of
263
275
  > `killOthers`. Default values (empty strings or objects) are returned for the fields that were not specified.
264
276
 
265
277
  Example:
@@ -55,6 +55,13 @@ const args = yargs
55
55
  describe: 'Disables colors from logging',
56
56
  type: 'boolean'
57
57
  },
58
+ 'hide': {
59
+ describe:
60
+ 'Comma-separated list of processes to hide the output.\n' +
61
+ 'The processes can be identified by their name or index.',
62
+ default: defaults.hide,
63
+ type: 'string'
64
+ },
58
65
 
59
66
  // Kill others
60
67
  'k': {
@@ -135,7 +142,7 @@ const args = yargs
135
142
  'Can be either the index or the name of the process.'
136
143
  }
137
144
  })
138
- .group(['m', 'n', 'name-separator', 'raw', 's', 'no-color'], 'General')
145
+ .group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide'], 'General')
139
146
  .group(['p', 'c', 'l', 't'], 'Prefix styling')
140
147
  .group(['i', 'default-input-target'], 'Input handling')
141
148
  .group(['k', 'kill-others-on-fail'], 'Killing other processes')
@@ -144,32 +151,27 @@ const args = yargs
144
151
  .epilogue(fs.readFileSync(__dirname + '/epilogue.txt', { encoding: 'utf8' }))
145
152
  .argv;
146
153
 
147
- const prefixColors = args.prefixColors.split(',');
148
154
  const names = (args.names || '').split(args.nameSeparator);
149
155
 
150
- let lastColor;
151
- concurrently(args._.map((command, index) => {
152
- // Use documented behaviour of repeating last colour when specifying more commands than colours
153
- lastColor = prefixColors[index] || lastColor;
154
- return {
155
- command,
156
- prefixColor: lastColor,
157
- name: names[index]
158
- };
159
- }), {
160
- inputStream: args.handleInput && process.stdin,
156
+ concurrently(args._.map((command, index) => ({
157
+ command,
158
+ name: names[index]
159
+ })), {
160
+ handleInput: args.handleInput,
161
161
  defaultInputTarget: args.defaultInputTarget,
162
162
  killOthers: args.killOthers
163
163
  ? ['success', 'failure']
164
164
  : (args.killOthersOnFail ? ['failure'] : []),
165
165
  maxProcesses: args.maxProcesses,
166
166
  raw: args.raw,
167
+ hide: args.hide.split(','),
167
168
  prefix: args.prefix,
169
+ prefixColors: args.prefixColors.split(','),
168
170
  prefixLength: args.prefixLength,
169
171
  restartDelay: args.restartAfter,
170
172
  restartTries: args.restartTries,
171
173
  successCondition: args.success,
172
- timestampFormat: args.timestampFormat
174
+ timestampFormat: args.timestampFormat,
173
175
  }).then(
174
176
  () => process.exit(0),
175
177
  () => process.exit(1)
@@ -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"');
@@ -193,7 +213,7 @@ describe('--names', () => {
193
213
  });
194
214
 
195
215
  describe('--prefix', () => {
196
- it('is alised to -p', done => {
216
+ it('is aliased to -p', done => {
197
217
  const child = run('-p command "echo foo" "echo bar"');
198
218
  child.log.pipe(buffer(child.close)).subscribe(lines => {
199
219
  expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
@@ -213,7 +233,7 @@ describe('--prefix', () => {
213
233
  });
214
234
 
215
235
  describe('--prefix-length', () => {
216
- it('is alised to -l', done => {
236
+ it('is aliased to -l', done => {
217
237
  const child = run('-p command -l 5 "echo foo" "echo bar"');
218
238
  child.log.pipe(buffer(child.close)).subscribe(lines => {
219
239
  expect(lines).toContainEqual(expect.stringContaining('[ec..o] foo'));
@@ -247,7 +267,7 @@ describe('--restart-tries', () => {
247
267
  });
248
268
 
249
269
  describe('--kill-others', () => {
250
- it('is alised to -k', done => {
270
+ it('is aliased to -k', done => {
251
271
  const child = run('-k "sleep 10" "exit 0"');
252
272
  child.log.pipe(buffer(child.close)).subscribe(lines => {
253
273
  expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
package/bin/epilogue.txt CHANGED
@@ -39,4 +39,4 @@ Examples:
39
39
 
40
40
  $ $0 npm:watch-*
41
41
 
42
- For more details, visit https://github.com/kimmobrunfeldt/concurrently
42
+ For more details, visit https://github.com/open-cli-tools/concurrently
package/index.js CHANGED
@@ -9,8 +9,9 @@ const RestartProcess = require('./src/flow-control/restart-process');
9
9
  const concurrently = require('./src/concurrently');
10
10
  const Logger = require('./src/logger');
11
11
 
12
- module.exports = (commands, options = {}) => {
12
+ module.exports = exports = (commands, options = {}) => {
13
13
  const logger = new Logger({
14
+ hide: options.hide,
14
15
  outputStream: options.outputStream || process.stdout,
15
16
  prefixFormat: options.prefix,
16
17
  prefixLength: options.prefixLength,
@@ -30,7 +31,8 @@ module.exports = (commands, options = {}) => {
30
31
  new InputHandler({
31
32
  logger,
32
33
  defaultInputTarget: options.defaultInputTarget,
33
- inputStream: options.inputStream,
34
+ inputStream: options.inputStream || (options.handleInput && process.stdin),
35
+ pauseInputStreamOnFinish: options.pauseInputStreamOnFinish,
34
36
  }),
35
37
  new KillOnSignal({ process }),
36
38
  new RestartProcess({
@@ -42,7 +44,8 @@ module.exports = (commands, options = {}) => {
42
44
  logger,
43
45
  conditions: options.killOthers
44
46
  })
45
- ]
47
+ ],
48
+ prefixColors: options.prefixColors || []
46
49
  });
47
50
  };
48
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "concurrently",
3
- "version": "6.2.0",
3
+ "version": "6.4.0",
4
4
  "description": "Run commands concurrently",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/kimmobrunfeldt/concurrently.git"
19
+ "url": "https://github.com/open-cli-tools/concurrently.git"
20
20
  },
21
21
  "keywords": [
22
22
  "bash",
@@ -32,7 +32,6 @@
32
32
  "chalk": "^4.1.0",
33
33
  "date-fns": "^2.16.1",
34
34
  "lodash": "^4.17.21",
35
- "read-pkg": "^5.2.0",
36
35
  "rxjs": "^6.6.3",
37
36
  "spawn-command": "^0.0.2-1",
38
37
  "supports-color": "^8.1.0",
@@ -1,8 +1,17 @@
1
1
  const _ = require('lodash');
2
- const readPkg = require('read-pkg');
2
+ const fs = require('fs');
3
3
 
4
4
  module.exports = class ExpandNpmWildcard {
5
- constructor(readPackage = readPkg.sync) {
5
+ static readPackage() {
6
+ try {
7
+ const json = fs.readFileSync('package.json', { encoding: 'utf-8' });
8
+ return JSON.parse(json);
9
+ } catch (e) {
10
+ return {};
11
+ }
12
+ }
13
+
14
+ constructor(readPackage = ExpandNpmWildcard.readPackage) {
6
15
  this.readPackage = readPackage;
7
16
  }
8
17
 
@@ -32,27 +32,40 @@ module.exports = (commands, options) => {
32
32
  new ExpandNpmWildcard()
33
33
  ];
34
34
 
35
+ let lastColor = '';
35
36
  commands = _(commands)
36
37
  .map(mapToCommandInfo)
37
38
  .flatMap(command => parseCommand(command, commandParsers))
38
- .map((command, index) => new Command(
39
- Object.assign({
40
- index,
41
- spawnOpts: getSpawnOpts({
42
- raw: options.raw,
43
- env: command.env,
44
- cwd: command.cwd || options.cwd,
45
- }),
46
- killProcess: options.kill,
47
- spawn: options.spawn,
48
- }, command)
49
- ))
39
+ .map((command, index) => {
40
+ // Use documented behaviour of repeating last color when specifying more commands than colors
41
+ lastColor = options.prefixColors && options.prefixColors[index] || lastColor;
42
+ return new Command(
43
+ Object.assign({
44
+ index,
45
+ spawnOpts: getSpawnOpts({
46
+ raw: options.raw,
47
+ env: command.env,
48
+ cwd: command.cwd || options.cwd,
49
+ }),
50
+ prefixColor: lastColor,
51
+ killProcess: options.kill,
52
+ spawn: options.spawn,
53
+ }, command)
54
+ );
55
+ })
50
56
  .value();
51
57
 
52
- commands = options.controllers.reduce(
53
- (prevCommands, controller) => controller.handle(prevCommands),
54
- commands
58
+ const handleResult = options.controllers.reduce(
59
+ ({ commands: prevCommands, onFinishCallbacks }, controller) => {
60
+ const { commands, onFinish } = controller.handle(prevCommands);
61
+ return {
62
+ commands,
63
+ onFinishCallbacks: _.concat(onFinishCallbacks, onFinish ? [onFinish] : [])
64
+ };
65
+ },
66
+ { commands, onFinishCallbacks: [] }
55
67
  );
68
+ commands = handleResult.commands;
56
69
 
57
70
  const commandsLeft = commands.slice();
58
71
  const maxProcesses = Math.max(1, Number(options.maxProcesses) || commandsLeft.length);
@@ -60,17 +73,22 @@ module.exports = (commands, options) => {
60
73
  maybeRunMore(commandsLeft);
61
74
  }
62
75
 
63
- return new CompletionListener({ successCondition: options.successCondition }).listen(commands);
76
+ return new CompletionListener({ successCondition: options.successCondition })
77
+ .listen(commands)
78
+ .finally(() => {
79
+ handleResult.onFinishCallbacks.forEach((onFinish) => onFinish());
80
+ });
64
81
  };
65
82
 
66
83
  function mapToCommandInfo(command) {
67
- return {
84
+ return Object.assign({
68
85
  command: command.command || command,
69
86
  name: command.name || '',
70
- prefixColor: command.prefixColor || '',
71
87
  env: command.env || {},
72
88
  cwd: command.cwd || '',
73
- };
89
+ }, command.prefixColor ? {
90
+ prefixColor: command.prefixColor,
91
+ } : {});
74
92
  }
75
93
 
76
94
  function parseCommand(command, parsers) {
@@ -1,6 +1,7 @@
1
1
  const EventEmitter = require('events');
2
2
 
3
3
  const createFakeCommand = require('./flow-control/fixtures/fake-command');
4
+ const FakeHandler = require('./flow-control/fixtures/fake-handler');
4
5
  const concurrently = require('./concurrently');
5
6
 
6
7
  let spawn, kill, controllers, processes = [];
@@ -18,7 +19,7 @@ beforeEach(() => {
18
19
  return process;
19
20
  });
20
21
  kill = jest.fn();
21
- controllers = [{ handle: jest.fn(arg => arg) }, { handle: jest.fn(arg => arg) }];
22
+ controllers = [new FakeHandler(), new FakeHandler()];
22
23
  });
23
24
 
24
25
  it('fails if commands is not an array', () => {
@@ -83,9 +84,22 @@ it('runs commands with a name or prefix color', () => {
83
84
  });
84
85
  });
85
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
+
86
100
  it('passes commands wrapped from a controller to the next one', () => {
87
101
  const fakeCommand = createFakeCommand('banana', 'banana');
88
- controllers[0].handle.mockReturnValue([fakeCommand]);
102
+ controllers[0].handle.mockReturnValue({ commands: [fakeCommand] });
89
103
 
90
104
  create(['echo']);
91
105
 
@@ -165,3 +179,21 @@ it('uses overridden cwd option for each command if specified', () => {
165
179
  cwd: 'foobar',
166
180
  }));
167
181
  });
182
+
183
+ it('runs onFinish hook after all commands run', async () => {
184
+ const promise = create(['foo', 'bar'], { maxProcesses: 1 });
185
+ expect(spawn).toHaveBeenCalledTimes(1);
186
+ expect(controllers[0].onFinish).not.toHaveBeenCalled();
187
+ expect(controllers[1].onFinish).not.toHaveBeenCalled();
188
+
189
+ processes[0].emit('close', 0, null);
190
+ expect(spawn).toHaveBeenCalledTimes(2);
191
+ expect(controllers[0].onFinish).not.toHaveBeenCalled();
192
+ expect(controllers[1].onFinish).not.toHaveBeenCalled();
193
+
194
+ processes[1].emit('close', 0, null);
195
+ await promise;
196
+
197
+ expect(controllers[0].onFinish).toHaveBeenCalled();
198
+ expect(controllers[1].onFinish).toHaveBeenCalled();
199
+ });
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: '',
@@ -0,0 +1,16 @@
1
+ module.exports = class BaseHandler {
2
+ constructor(options = {}) {
3
+ const { logger } = options;
4
+
5
+ this.logger = logger;
6
+ }
7
+
8
+ handle(commands) {
9
+ return {
10
+ commands,
11
+ // an optional callback to call when all commands have finished
12
+ // (either successful or not)
13
+ onFinish: null,
14
+ };
15
+ }
16
+ };
@@ -2,17 +2,20 @@ const Rx = require('rxjs');
2
2
  const { map } = require('rxjs/operators');
3
3
 
4
4
  const defaults = require('../defaults');
5
+ const BaseHandler = require('./base-handler');
6
+
7
+ module.exports = class InputHandler extends BaseHandler {
8
+ constructor({ defaultInputTarget, inputStream, pauseInputStreamOnFinish, logger }) {
9
+ super({ logger });
5
10
 
6
- module.exports = class InputHandler {
7
- constructor({ defaultInputTarget, inputStream, logger }) {
8
11
  this.defaultInputTarget = defaultInputTarget || defaults.defaultInputTarget;
9
12
  this.inputStream = inputStream;
10
- this.logger = logger;
13
+ this.pauseInputStreamOnFinish = pauseInputStreamOnFinish !== false;
11
14
  }
12
15
 
13
16
  handle(commands) {
14
17
  if (!this.inputStream) {
15
- return commands;
18
+ return { commands };
16
19
  }
17
20
 
18
21
  Rx.fromEvent(this.inputStream, 'data')
@@ -34,6 +37,14 @@ module.exports = class InputHandler {
34
37
  }
35
38
  });
36
39
 
37
- return commands;
40
+ return {
41
+ commands,
42
+ onFinish: () => {
43
+ if (this.pauseInputStreamOnFinish) {
44
+ // https://github.com/kimmobrunfeldt/concurrently/issues/252
45
+ this.inputStream.pause();
46
+ }
47
+ },
48
+ };
38
49
  }
39
50
  };
@@ -1,4 +1,4 @@
1
- const EventEmitter = require('events');
1
+ const stream = require('stream');
2
2
  const { createMockInstance } = require('jest-create-mock-instance');
3
3
 
4
4
  const Logger = require('../logger');
@@ -12,7 +12,7 @@ beforeEach(() => {
12
12
  createFakeCommand('foo', 'echo foo', 0),
13
13
  createFakeCommand('bar', 'echo bar', 1),
14
14
  ];
15
- inputStream = new EventEmitter();
15
+ inputStream = new stream.PassThrough();
16
16
  logger = createMockInstance(Logger);
17
17
  controller = new InputHandler({
18
18
  defaultInputTarget: 0,
@@ -22,16 +22,16 @@ beforeEach(() => {
22
22
  });
23
23
 
24
24
  it('returns same commands', () => {
25
- expect(controller.handle(commands)).toBe(commands);
25
+ expect(controller.handle(commands)).toMatchObject({ commands });
26
26
 
27
27
  controller = new InputHandler({ logger });
28
- expect(controller.handle(commands)).toBe(commands);
28
+ expect(controller.handle(commands)).toMatchObject({ commands });
29
29
  });
30
30
 
31
31
  it('forwards input stream to default target ID', () => {
32
32
  controller.handle(commands);
33
33
 
34
- inputStream.emit('data', Buffer.from('something'));
34
+ inputStream.write('something');
35
35
 
36
36
  expect(commands[0].stdin.write).toHaveBeenCalledTimes(1);
37
37
  expect(commands[0].stdin.write).toHaveBeenCalledWith('something');
@@ -41,7 +41,7 @@ it('forwards input stream to default target ID', () => {
41
41
  it('forwards input stream to target index specified in input', () => {
42
42
  controller.handle(commands);
43
43
 
44
- inputStream.emit('data', Buffer.from('1:something'));
44
+ inputStream.write('1:something');
45
45
 
46
46
  expect(commands[0].stdin.write).not.toHaveBeenCalled();
47
47
  expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
@@ -63,7 +63,7 @@ it('forwards input stream to target index specified in input when input contains
63
63
  it('forwards input stream to target name specified in input', () => {
64
64
  controller.handle(commands);
65
65
 
66
- inputStream.emit('data', Buffer.from('bar:something'));
66
+ inputStream.write('bar:something');
67
67
 
68
68
  expect(commands[0].stdin.write).not.toHaveBeenCalled();
69
69
  expect(commands[1].stdin.write).toHaveBeenCalledTimes(1);
@@ -74,7 +74,7 @@ it('logs error if command has no stdin open', () => {
74
74
  commands[0].stdin = null;
75
75
  controller.handle(commands);
76
76
 
77
- inputStream.emit('data', Buffer.from('something'));
77
+ inputStream.write('something');
78
78
 
79
79
  expect(commands[1].stdin.write).not.toHaveBeenCalled();
80
80
  expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command 0, or it has no stdin open\n');
@@ -83,9 +83,31 @@ it('logs error if command has no stdin open', () => {
83
83
  it('logs error if command is not found', () => {
84
84
  controller.handle(commands);
85
85
 
86
- inputStream.emit('data', Buffer.from('foobar:something'));
86
+ inputStream.write('foobar:something');
87
87
 
88
88
  expect(commands[0].stdin.write).not.toHaveBeenCalled();
89
89
  expect(commands[1].stdin.write).not.toHaveBeenCalled();
90
90
  expect(logger.logGlobalEvent).toHaveBeenCalledWith('Unable to find command foobar, or it has no stdin open\n');
91
91
  });
92
+
93
+ it('pauses input stream when finished', () => {
94
+ expect(inputStream.readableFlowing).toBeNull();
95
+
96
+ const { onFinish } = controller.handle(commands);
97
+ expect(inputStream.readableFlowing).toBe(true);
98
+
99
+ onFinish();
100
+ expect(inputStream.readableFlowing).toBe(false);
101
+ });
102
+
103
+ it('does not pause input stream when pauseInputStreamOnFinish is set to false', () => {
104
+ controller = new InputHandler({ inputStream, pauseInputStreamOnFinish: false });
105
+
106
+ expect(inputStream.readableFlowing).toBeNull();
107
+
108
+ const { onFinish } = controller.handle(commands);
109
+ expect(inputStream.readableFlowing).toBe(true);
110
+
111
+ onFinish();
112
+ expect(inputStream.readableFlowing).toBe(true);
113
+ });
@@ -1,8 +1,11 @@
1
1
  const { map } = require('rxjs/operators');
2
2
 
3
+ const BaseHandler = require('./base-handler');
3
4
 
4
- module.exports = class KillOnSignal {
5
+ module.exports = class KillOnSignal extends BaseHandler {
5
6
  constructor({ process }) {
7
+ super();
8
+
6
9
  this.process = process;
7
10
  }
8
11
 
@@ -15,16 +18,18 @@ module.exports = class KillOnSignal {
15
18
  });
16
19
  });
17
20
 
18
- return commands.map(command => {
19
- const closeStream = command.close.pipe(map(exitInfo => {
20
- const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;
21
- return Object.assign({}, exitInfo, { exitCode });
22
- }));
23
- return new Proxy(command, {
24
- get(target, prop) {
25
- return prop === 'close' ? closeStream : target[prop];
26
- }
27
- });
28
- });
21
+ return {
22
+ commands: commands.map(command => {
23
+ const closeStream = command.close.pipe(map(exitInfo => {
24
+ const exitCode = caughtSignal === 'SIGINT' ? 0 : exitInfo.exitCode;
25
+ return Object.assign({}, exitInfo, { exitCode });
26
+ }));
27
+ return new Proxy(command, {
28
+ get(target, prop) {
29
+ return prop === 'close' ? closeStream : target[prop];
30
+ }
31
+ });
32
+ })
33
+ };
29
34
  }
30
35
  };
@@ -14,7 +14,7 @@ beforeEach(() => {
14
14
  });
15
15
 
16
16
  it('returns commands that keep non-close streams from original commands', () => {
17
- const newCommands = controller.handle(commands);
17
+ const { commands: newCommands } = controller.handle(commands);
18
18
  newCommands.forEach((newCommand, i) => {
19
19
  expect(newCommand.close).not.toBe(commands[i].close);
20
20
  expect(newCommand.error).toBe(commands[i].error);
@@ -24,7 +24,7 @@ it('returns commands that keep non-close streams from original commands', () =>
24
24
  });
25
25
 
26
26
  it('returns commands that map SIGINT to exit code 0', () => {
27
- const newCommands = controller.handle(commands);
27
+ const { commands: newCommands } = controller.handle(commands);
28
28
  expect(newCommands).not.toBe(commands);
29
29
  expect(newCommands).toHaveLength(commands.length);
30
30
 
@@ -40,7 +40,7 @@ it('returns commands that map SIGINT to exit code 0', () => {
40
40
  });
41
41
 
42
42
  it('returns commands that keep non-SIGINT exit codes', () => {
43
- const newCommands = controller.handle(commands);
43
+ const { commands: newCommands } = controller.handle(commands);
44
44
  expect(newCommands).not.toBe(commands);
45
45
  expect(newCommands).toHaveLength(commands.length);
46
46
 
@@ -1,9 +1,12 @@
1
1
  const _ = require('lodash');
2
2
  const { filter, map } = require('rxjs/operators');
3
3
 
4
- module.exports = class KillOthers {
4
+ const BaseHandler = require('./base-handler');
5
+
6
+ module.exports = class KillOthers extends BaseHandler {
5
7
  constructor({ logger, conditions }) {
6
- this.logger = logger;
8
+ super({ logger });
9
+
7
10
  this.conditions = _.castArray(conditions);
8
11
  }
9
12
 
@@ -14,7 +17,7 @@ module.exports = class KillOthers {
14
17
  ));
15
18
 
16
19
  if (!conditions.length) {
17
- return commands;
20
+ return { commands };
18
21
  }
19
22
 
20
23
  const closeStates = commands.map(command => command.close.pipe(
@@ -30,6 +33,6 @@ module.exports = class KillOthers {
30
33
  }
31
34
  }));
32
35
 
33
- return commands;
36
+ return { commands };
34
37
  }
35
38
  };
@@ -20,8 +20,8 @@ const createWithConditions = conditions => new KillOthers({
20
20
  });
21
21
 
22
22
  it('returns same commands', () => {
23
- expect(createWithConditions(['foo']).handle(commands)).toBe(commands);
24
- expect(createWithConditions(['failure']).handle(commands)).toBe(commands);
23
+ expect(createWithConditions(['foo']).handle(commands)).toMatchObject({ commands });
24
+ expect(createWithConditions(['failure']).handle(commands)).toMatchObject({ commands });
25
25
  });
26
26
 
27
27
  it('does not kill others if condition does not match', () => {
@@ -1,10 +1,8 @@
1
1
  const { of } = require('rxjs');
2
2
 
3
- module.exports = class LogExit {
4
- constructor({ logger }) {
5
- this.logger = logger;
6
- }
3
+ const BaseHandler = require('./base-handler');
7
4
 
5
+ module.exports = class LogExit extends BaseHandler {
8
6
  handle(commands) {
9
7
  commands.forEach(command => command.error.subscribe(event => {
10
8
  this.logger.logCommandEvent(
@@ -15,6 +13,6 @@ module.exports = class LogExit {
15
13
  this.logger.logCommandEvent(event.stack || event, command);
16
14
  }));
17
15
 
18
- return commands;
16
+ return { commands };
19
17
  }
20
18
  };
@@ -15,7 +15,7 @@ beforeEach(() => {
15
15
  });
16
16
 
17
17
  it('returns same commands', () => {
18
- expect(controller.handle(commands)).toBe(commands);
18
+ expect(controller.handle(commands)).toMatchObject({ commands });
19
19
  });
20
20
 
21
21
  it('logs the error event of each command', () => {
@@ -1,13 +1,11 @@
1
- module.exports = class LogExit {
2
- constructor({ logger }) {
3
- this.logger = logger;
4
- }
1
+ const BaseHandler = require('./base-handler');
5
2
 
3
+ module.exports = class LogExit extends BaseHandler {
6
4
  handle(commands) {
7
5
  commands.forEach(command => command.close.subscribe(({ exitCode }) => {
8
6
  this.logger.logCommandEvent(`${command.command} exited with code ${exitCode}`, command);
9
7
  }));
10
8
 
11
- return commands;
9
+ return { commands };
12
10
  }
13
11
  };
@@ -15,7 +15,7 @@ beforeEach(() => {
15
15
  });
16
16
 
17
17
  it('returns same commands', () => {
18
- expect(controller.handle(commands)).toBe(commands);
18
+ expect(controller.handle(commands)).toMatchObject({ commands });
19
19
  });
20
20
 
21
21
  it('logs the close event of each command', () => {
@@ -1,14 +1,12 @@
1
- module.exports = class LogOutput {
2
- constructor({ logger }) {
3
- this.logger = logger;
4
- }
1
+ const BaseHandler = require('./base-handler');
5
2
 
3
+ module.exports = class LogOutput extends BaseHandler {
6
4
  handle(commands) {
7
5
  commands.forEach(command => {
8
6
  command.stdout.subscribe(text => this.logger.logCommandText(text.toString(), command));
9
7
  command.stderr.subscribe(text => this.logger.logCommandText(text.toString(), command));
10
8
  });
11
9
 
12
- return commands;
10
+ return { commands };
13
11
  }
14
12
  };
@@ -15,7 +15,7 @@ beforeEach(() => {
15
15
  });
16
16
 
17
17
  it('returns same commands', () => {
18
- expect(controller.handle(commands)).toBe(commands);
18
+ expect(controller.handle(commands)).toMatchObject({ commands });
19
19
  });
20
20
 
21
21
  it('logs the stdout of each command', () => {
@@ -2,19 +2,21 @@ const Rx = require('rxjs');
2
2
  const { defaultIfEmpty, delay, filter, mapTo, skip, take, takeWhile } = require('rxjs/operators');
3
3
 
4
4
  const defaults = require('../defaults');
5
+ const BaseHandler = require('./base-handler');
5
6
 
6
- module.exports = class RestartProcess {
7
+ module.exports = class RestartProcess extends BaseHandler {
7
8
  constructor({ delay, tries, logger, scheduler }) {
9
+ super({ logger });
10
+
8
11
  this.delay = +delay || defaults.restartDelay;
9
12
  this.tries = +tries || defaults.restartTries;
10
13
  this.tries = this.tries < 0 ? Infinity : this.tries;
11
- this.logger = logger;
12
14
  this.scheduler = scheduler;
13
15
  }
14
16
 
15
17
  handle(commands) {
16
18
  if (this.tries === 0) {
17
- return commands;
19
+ return { commands };
18
20
  }
19
21
 
20
22
  commands.map(command => command.close.pipe(
@@ -36,17 +38,19 @@ module.exports = class RestartProcess {
36
38
  }
37
39
  }));
38
40
 
39
- return commands.map(command => {
40
- const closeStream = command.close.pipe(filter(({ exitCode }, emission) => {
41
- // We let all success codes pass, and failures only after restarting won't happen again
42
- return exitCode === 0 || emission >= this.tries;
43
- }));
41
+ return {
42
+ commands: commands.map(command => {
43
+ const closeStream = command.close.pipe(filter(({ exitCode }, emission) => {
44
+ // We let all success codes pass, and failures only after restarting won't happen again
45
+ return exitCode === 0 || emission >= this.tries;
46
+ }));
44
47
 
45
- return new Proxy(command, {
46
- get(target, prop) {
47
- return prop === 'close' ? closeStream : target[prop];
48
- }
49
- });
50
- });
48
+ return new Proxy(command, {
49
+ get(target, prop) {
50
+ return prop === 'close' ? closeStream : target[prop];
51
+ }
52
+ });
53
+ })
54
+ };
51
55
  }
52
56
  };
@@ -91,17 +91,17 @@ it('restarts processes until they succeed', () => {
91
91
  describe('returned commands', () => {
92
92
  it('are the same if 0 tries are to be attempted', () => {
93
93
  controller = new RestartProcess({ logger, scheduler });
94
- expect(controller.handle(commands)).toBe(commands);
94
+ expect(controller.handle(commands)).toMatchObject({ commands });
95
95
  });
96
96
 
97
97
  it('are not the same, but with same length if 1+ tries are to be attempted', () => {
98
- const newCommands = controller.handle(commands);
98
+ const { commands: newCommands } = controller.handle(commands);
99
99
  expect(newCommands).not.toBe(commands);
100
100
  expect(newCommands).toHaveLength(commands.length);
101
101
  });
102
102
 
103
103
  it('skip close events followed by restarts', () => {
104
- const newCommands = controller.handle(commands);
104
+ const { commands: newCommands } = controller.handle(commands);
105
105
 
106
106
  const callback = jest.fn();
107
107
  newCommands[0].close.subscribe(callback);
@@ -120,7 +120,7 @@ describe('returned commands', () => {
120
120
  });
121
121
 
122
122
  it('keep non-close streams from original commands', () => {
123
- const newCommands = controller.handle(commands);
123
+ const { commands: newCommands } = controller.handle(commands);
124
124
  newCommands.forEach((newCommand, i) => {
125
125
  expect(newCommand.close).not.toBe(commands[i].close);
126
126
  expect(newCommand.error).toBe(commands[i].error);
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
  }
@@ -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 });