concurrently 3.6.0 → 4.1.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.
Files changed (52) hide show
  1. package/.travis.yml +3 -4
  2. package/.vscode/settings.json +5 -0
  3. package/CONTRIBUTING.md +1 -4
  4. package/README.md +131 -94
  5. package/appveyor.yml +1 -1
  6. package/bin/concurrently.js +164 -0
  7. package/bin/concurrently.spec.js +340 -0
  8. package/bin/epilogue.txt +46 -0
  9. package/{test/support → bin/fixtures}/read-echo.js +0 -2
  10. package/index.js +56 -0
  11. package/package.json +28 -31
  12. package/src/command-parser/expand-npm-shortcut.js +13 -0
  13. package/src/command-parser/expand-npm-shortcut.spec.js +36 -0
  14. package/src/command-parser/expand-npm-wildcard.js +34 -0
  15. package/src/command-parser/expand-npm-wildcard.spec.js +58 -0
  16. package/src/command-parser/strip-quotes.js +12 -0
  17. package/src/command-parser/strip-quotes.spec.js +14 -0
  18. package/src/command.js +50 -0
  19. package/src/command.spec.js +142 -0
  20. package/src/completion-listener.js +35 -0
  21. package/src/completion-listener.spec.js +88 -0
  22. package/src/concurrently.js +69 -0
  23. package/src/concurrently.spec.js +77 -0
  24. package/src/defaults.js +27 -0
  25. package/src/flow-control/fixtures/fake-command.js +16 -0
  26. package/src/flow-control/input-handler.js +39 -0
  27. package/src/flow-control/input-handler.spec.js +79 -0
  28. package/src/flow-control/kill-on-signal.js +29 -0
  29. package/src/flow-control/kill-on-signal.spec.js +70 -0
  30. package/src/flow-control/kill-others.js +35 -0
  31. package/src/flow-control/kill-others.spec.js +66 -0
  32. package/src/flow-control/log-error.js +20 -0
  33. package/src/flow-control/log-error.spec.js +40 -0
  34. package/src/flow-control/log-exit.js +13 -0
  35. package/src/flow-control/log-exit.spec.js +36 -0
  36. package/src/flow-control/log-output.js +14 -0
  37. package/src/flow-control/log-output.spec.js +41 -0
  38. package/src/flow-control/restart-process.js +51 -0
  39. package/src/flow-control/restart-process.spec.js +129 -0
  40. package/src/get-spawn-opts.js +12 -0
  41. package/src/get-spawn-opts.spec.js +18 -0
  42. package/src/logger.js +109 -0
  43. package/src/logger.spec.js +178 -0
  44. package/src/findChild.js +0 -11
  45. package/src/main.js +0 -563
  46. package/src/parseCmds.js +0 -65
  47. package/src/pkgInfo.js +0 -17
  48. package/test/support/signal.js +0 -13
  49. package/test/test-findChild.js +0 -39
  50. package/test/test-functional.js +0 -396
  51. package/test/test-parseCmds.js +0 -204
  52. package/test/utils.js +0 -68
@@ -0,0 +1,340 @@
1
+ const readline = require('readline');
2
+ const _ = require('lodash');
3
+ const Rx = require('rxjs');
4
+ const { buffer, map } = require('rxjs/operators');
5
+ const spawn = require('spawn-command');
6
+
7
+ const isWindows = process.platform === 'win32';
8
+ const createKillMessage = prefix => new RegExp(
9
+ _.escapeRegExp(prefix) +
10
+ ' exited with code ' +
11
+ (isWindows ? 1 : '(SIGTERM|143)')
12
+ );
13
+
14
+ const run = args => {
15
+ const child = spawn('node ./concurrently.js ' + args, {
16
+ cwd: __dirname
17
+ });
18
+
19
+ const stdout = readline.createInterface({
20
+ input: child.stdout,
21
+ output: null
22
+ });
23
+
24
+ const stderr = readline.createInterface({
25
+ input: child.stderr,
26
+ output: null
27
+ });
28
+
29
+ const close = Rx.fromEvent(child, 'close');
30
+ const log = Rx.merge(
31
+ Rx.fromEvent(stdout, 'line'),
32
+ Rx.fromEvent(stderr, 'line')
33
+ ).pipe(map(data => data.toString()));
34
+
35
+ return {
36
+ close,
37
+ log,
38
+ stdin: child.stdin,
39
+ pid: child.pid
40
+ };
41
+ };
42
+
43
+ it('has help command', done => {
44
+ run('--help').close.subscribe(event => {
45
+ expect(event[0]).toBe(0);
46
+ done();
47
+ }, done);
48
+ });
49
+
50
+ it('has version command', done => {
51
+ Rx.combineLatest(
52
+ run('--version').close,
53
+ run('-V').close,
54
+ run('-v').close
55
+ ).subscribe(events => {
56
+ expect(events[0][0]).toBe(0);
57
+ expect(events[1][0]).toBe(0);
58
+ expect(events[2][0]).toBe(0);
59
+ done();
60
+ }, done);
61
+ });
62
+
63
+ describe('exiting conditions', () => {
64
+ it('is of success by default when running successful commands', done => {
65
+ run('"echo foo" "echo bar"')
66
+ .close
67
+ .subscribe(exit => {
68
+ expect(exit[0]).toBe(0);
69
+ done();
70
+ }, done);
71
+ });
72
+
73
+ it('is of failure by default when one of the command fails', done => {
74
+ run('"echo foo" "exit 1"')
75
+ .close
76
+ .subscribe(exit => {
77
+ expect(exit[0]).toBeGreaterThan(0);
78
+ done();
79
+ }, done);
80
+ });
81
+
82
+ it('is of success when --success=first and first command to exit succeeds', done => {
83
+ run('--success=first "echo foo" "sleep 0.5 && exit 1"')
84
+ .close
85
+ .subscribe(exit => {
86
+ expect(exit[0]).toBe(0);
87
+ done();
88
+ }, done);
89
+ });
90
+
91
+ it('is of failure when --success=first and first command to exit fails', done => {
92
+ run('--success=first "exit 1" "sleep 0.5 && echo foo"')
93
+ .close
94
+ .subscribe(exit => {
95
+ expect(exit[0]).toBeGreaterThan(0);
96
+ done();
97
+ }, done);
98
+ });
99
+
100
+ it('is of success when --success=last and last command to exit succeeds', done => {
101
+ run('--success=last "exit 1" "sleep 0.5 && echo foo"')
102
+ .close
103
+ .subscribe(exit => {
104
+ expect(exit[0]).toBe(0);
105
+ done();
106
+ }, done);
107
+ });
108
+
109
+ it('is of failure when --success=last and last command to exit fails', done => {
110
+ run('--success=last "echo foo" "sleep 0.5 && exit 1"')
111
+ .close
112
+ .subscribe(exit => {
113
+ expect(exit[0]).toBeGreaterThan(0);
114
+ done();
115
+ }, done);
116
+ });
117
+
118
+ it.skip('is of success when a SIGINT is sent', done => {
119
+ const child = run('"node fixtures/read-echo.js"');
120
+ child.close.subscribe(exit => {
121
+ // TODO This is null within Node, but should be 0 outside (eg from real terminal)
122
+ expect(exit[0]).toBe(0);
123
+ done();
124
+ }, done);
125
+
126
+ process.kill(child.pid, 'SIGINT');
127
+ });
128
+
129
+ it('is aliased to -s', done => {
130
+ run('-s last "exit 1" "sleep 0.5 && echo foo"')
131
+ .close
132
+ .subscribe(exit => {
133
+ expect(exit[0]).toBe(0);
134
+ done();
135
+ }, done);
136
+ });
137
+ });
138
+
139
+ describe('--raw', () => {
140
+ it('is aliased to -r', done => {
141
+ const child = run('-r "echo foo" "echo bar"');
142
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
143
+ expect(lines).toHaveLength(2);
144
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
145
+ expect(lines).toContainEqual(expect.stringContaining('bar'));
146
+ done();
147
+ }, done);
148
+ });
149
+
150
+ it('does not log any extra output', done => {
151
+ const child = run('--raw "echo foo" "echo bar"');
152
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
153
+ expect(lines).toHaveLength(2);
154
+ expect(lines).toContainEqual(expect.stringContaining('foo'));
155
+ expect(lines).toContainEqual(expect.stringContaining('bar'));
156
+ done();
157
+ }, done);
158
+ });
159
+ });
160
+
161
+ describe('--names', () => {
162
+ it('is aliased to -n', done => {
163
+ const child = run('-n foo,bar "echo foo" "echo bar"');
164
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
165
+ expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
166
+ expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
167
+ done();
168
+ }, done);
169
+ });
170
+
171
+ it('prefixes with names', done => {
172
+ const child = run('--names foo,bar "echo foo" "echo bar"');
173
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
174
+ expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
175
+ expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
176
+ done();
177
+ }, done);
178
+ });
179
+
180
+ it('is split using --name-separator arg', done => {
181
+ const child = run('--names "foo|bar" --name-separator "|" "echo foo" "echo bar"');
182
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
183
+ expect(lines).toContainEqual(expect.stringContaining('[foo] foo'));
184
+ expect(lines).toContainEqual(expect.stringContaining('[bar] bar'));
185
+ done();
186
+ }, done);
187
+ });
188
+ });
189
+
190
+ describe('--prefix', () => {
191
+ it('is alised to -p', done => {
192
+ const child = run('-p command "echo foo" "echo bar"');
193
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
194
+ expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
195
+ expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
196
+ done();
197
+ }, done);
198
+ });
199
+
200
+ it('specifies custom prefix', done => {
201
+ const child = run('--prefix command "echo foo" "echo bar"');
202
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
203
+ expect(lines).toContainEqual(expect.stringContaining('[echo foo] foo'));
204
+ expect(lines).toContainEqual(expect.stringContaining('[echo bar] bar'));
205
+ done();
206
+ }, done);
207
+ });
208
+ });
209
+
210
+ describe('--restart-tries', () => {
211
+ it('changes how many times a command will restart', done => {
212
+ const child = run('--restart-tries 1 "exit 1"');
213
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
214
+ expect(lines).toEqual([
215
+ expect.stringContaining('[0] exit 1 exited with code 1'),
216
+ expect.stringContaining('[0] exit 1 restarted'),
217
+ expect.stringContaining('[0] exit 1 exited with code 1'),
218
+ ]);
219
+ done();
220
+ }, done);
221
+ });
222
+ });
223
+
224
+ describe('--kill-others', () => {
225
+ it('is alised to -k', done => {
226
+ const child = run('-k "sleep 10" "exit 0"');
227
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
228
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
229
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
230
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
231
+ done();
232
+ }, done);
233
+ });
234
+
235
+ it('kills on success', done => {
236
+ const child = run('--kill-others "sleep 10" "exit 0"');
237
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
238
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
239
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
240
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
241
+ done();
242
+ }, done);
243
+ });
244
+
245
+ it('kills on failure', done => {
246
+ const child = run('--kill-others "sleep 10" "exit 1"');
247
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
248
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
249
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
250
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
251
+ done();
252
+ }, done);
253
+ });
254
+ });
255
+
256
+ describe('--kill-others-on-fail', () => {
257
+ it('does not kill on success', done => {
258
+ const child = run('--kill-others-on-fail "sleep 0.5" "exit 0"');
259
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
260
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 0 exited with code 0'));
261
+ expect(lines).toContainEqual(expect.stringContaining('[0] sleep 0.5 exited with code 0'));
262
+ done();
263
+ }, done);
264
+ });
265
+
266
+ it('kills on failure', done => {
267
+ const child = run('--kill-others-on-fail "sleep 10" "exit 1"');
268
+ child.log.pipe(buffer(child.close)).subscribe(lines => {
269
+ expect(lines).toContainEqual(expect.stringContaining('[1] exit 1 exited with code 1'));
270
+ expect(lines).toContainEqual(expect.stringContaining('Sending SIGTERM to other processes'));
271
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] sleep 10')));
272
+ done();
273
+ }, done);
274
+ });
275
+ });
276
+
277
+ describe('--handle-input', () => {
278
+ it('is alised to -i', done => {
279
+ const child = run('-i "node fixtures/read-echo.js"');
280
+ child.log.subscribe(line => {
281
+ if (/READING/.test(line)) {
282
+ child.stdin.write('stop\n');
283
+ }
284
+
285
+ if (/\[0\] stop/.test(line)) {
286
+ done();
287
+ }
288
+ }, done);
289
+ });
290
+
291
+ it('forwards input to first process by default', done => {
292
+ const child = run('--handle-input "node fixtures/read-echo.js"');
293
+ child.log.subscribe(line => {
294
+ if (/READING/.test(line)) {
295
+ child.stdin.write('stop\n');
296
+ }
297
+
298
+ if (/\[0\] stop/.test(line)) {
299
+ done();
300
+ }
301
+ }, done);
302
+ });
303
+
304
+
305
+ it('forwards input to process --default-input-target', done => {
306
+ const lines = [];
307
+ const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
308
+ child.log.subscribe(line => {
309
+ lines.push(line);
310
+ if (/\[1\] READING/.test(line)) {
311
+ child.stdin.write('stop\n');
312
+ }
313
+ }, done);
314
+
315
+ child.close.subscribe(exit => {
316
+ expect(exit[0]).toBeGreaterThan(0);
317
+ expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
318
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
319
+ done();
320
+ }, done);
321
+ });
322
+
323
+ it('forwards input to specified process', done => {
324
+ const lines = [];
325
+ const child = run('-ki "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
326
+ child.log.subscribe(line => {
327
+ lines.push(line);
328
+ if (/\[1\] READING/.test(line)) {
329
+ child.stdin.write('1:stop\n');
330
+ }
331
+ }, done);
332
+
333
+ child.close.subscribe(exit => {
334
+ expect(exit[0]).toBeGreaterThan(0);
335
+ expect(lines).toContainEqual(expect.stringContaining('[1] stop'));
336
+ expect(lines).toContainEqual(expect.stringMatching(createKillMessage('[0] node fixtures/read-echo.js')));
337
+ done();
338
+ }, done);
339
+ });
340
+ });
@@ -0,0 +1,46 @@
1
+ Examples:
2
+
3
+ - Output nothing more than stdout+stderr of child processes
4
+
5
+ $ $0 --raw "npm run watch-less" "npm run watch-js"
6
+
7
+ - Normal output but without colors e.g. when logging to file
8
+
9
+ $ $0 --no-color "grunt watch" "http-server" > log
10
+
11
+ - Custom prefix
12
+
13
+ $ $0 --prefix "{time}-{pid}" "npm run watch" "http-server"
14
+
15
+ - Custom names and colored prefixes
16
+
17
+ $ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"
18
+
19
+ - Shortened NPM run commands
20
+
21
+ $ $0 npm:watch-node npm:watch-js npm:watch-css
22
+
23
+ - Send input to default
24
+
25
+ $ $0 --handle-input "nodemon" "npm run watch-js"
26
+ rs # Sends rs command to nodemon process
27
+
28
+ - Send input to specific child identified by index
29
+
30
+ $ $0 --handle-input "npm run watch-js" nodemon
31
+ 1:rs
32
+
33
+ - Send input to specific child identified by name
34
+
35
+ $ $0 --handle-input -n js,srv "npm run watch-js" nodemon
36
+ srv:rs
37
+
38
+ - Shortened NPM run commands
39
+
40
+ $ $0 npm:watch-node npm:watch-js npm:watch-css
41
+
42
+ - Shortened NPM run command with wildcard
43
+
44
+ $ $0 npm:watch-*
45
+
46
+ For more details, visit https://github.com/kimmobrunfeldt/concurrently
@@ -1,5 +1,3 @@
1
- 'use strict';
2
-
3
1
  process.stdin.on('data', (chunk) => {
4
2
  const line = chunk.toString().trim();
5
3
  console.log(line);
package/index.js ADDED
@@ -0,0 +1,56 @@
1
+ const InputHandler = require('./src/flow-control/input-handler');
2
+ const KillOnSignal = require('./src/flow-control/kill-on-signal');
3
+ const KillOthers = require('./src/flow-control/kill-others');
4
+ const LogError = require('./src/flow-control/log-error');
5
+ const LogExit = require('./src/flow-control/log-exit');
6
+ const LogOutput = require('./src/flow-control/log-output');
7
+ const RestartProcess = require('./src/flow-control/restart-process');
8
+
9
+ const concurrently = require('./src/concurrently');
10
+ const Logger = require('./src/logger');
11
+
12
+ module.exports = (commands, options = {}) => {
13
+ const logger = new Logger({
14
+ outputStream: options.outputStream || process.stdout,
15
+ prefixFormat: options.prefix,
16
+ prefixLength: options.prefixLength,
17
+ raw: options.raw,
18
+ timestampFormat: options.timestampFormat,
19
+ });
20
+
21
+ return concurrently(commands, {
22
+ raw: options.raw,
23
+ successCondition: options.successCondition,
24
+ controllers: [
25
+ new LogError({ logger }),
26
+ new LogOutput({ logger }),
27
+ new LogExit({ logger }),
28
+ new InputHandler({
29
+ logger,
30
+ defaultInputTarget: options.defaultInputTarget,
31
+ inputStream: options.inputStream,
32
+ }),
33
+ new KillOnSignal(),
34
+ new RestartProcess({
35
+ logger,
36
+ delay: options.restartDelay,
37
+ tries: options.restartTries,
38
+ }),
39
+ new KillOthers({
40
+ logger,
41
+ conditions: options.killOthers
42
+ })
43
+ ]
44
+ });
45
+ };
46
+
47
+ // Export all flow controllers and the main concurrently function,
48
+ // so that 3rd-parties can use them however they want
49
+ exports.concurrently = concurrently;
50
+ exports.InputHandler = InputHandler;
51
+ exports.KillOnSignal = KillOnSignal;
52
+ exports.KillOthers = KillOthers;
53
+ exports.LogError = LogError;
54
+ exports.LogExit = LogExit;
55
+ exports.LogOutput = LogOutput;
56
+ exports.RestartProcess = RestartProcess;
package/package.json CHANGED
@@ -1,22 +1,18 @@
1
1
  {
2
2
  "name": "concurrently",
3
- "version": "3.6.0",
3
+ "version": "4.1.0",
4
4
  "description": "Run commands concurrently",
5
- "main": "src/main.js",
5
+ "main": "index.js",
6
6
  "bin": {
7
- "concurrent": "./src/main.js",
8
- "concurrently": "./src/main.js"
7
+ "concurrently": "./bin/concurrently.js"
9
8
  },
10
9
  "engines": {
11
- "node": ">=4.0.0"
10
+ "node": ">=6.0.0"
12
11
  },
13
12
  "scripts": {
14
- "lint": "eslint .",
15
- "test": "mocha",
16
- "echo": "echo",
17
- "echo-test": "echo test",
18
- "echo-sound-beep": "echo beep",
19
- "echo-sound-boop": "echo boop"
13
+ "lint": "eslint . --ignore-path .gitignore",
14
+ "report-coverage": "cat coverage/lcov.info | coveralls",
15
+ "test": "jest"
20
16
  },
21
17
  "repository": {
22
18
  "type": "git",
@@ -32,31 +28,32 @@
32
28
  ],
33
29
  "author": "Kimmo Brunfeldt",
34
30
  "license": "MIT",
35
- "bugs": {
36
- "url": "https://github.com/kimmobrunfeldt/concurrently/issues"
37
- },
38
- "homepage": "https://github.com/kimmobrunfeldt/concurrently",
39
31
  "dependencies": {
40
32
  "chalk": "^2.4.1",
41
- "commander": "2.6.0",
42
33
  "date-fns": "^1.23.0",
43
- "lodash": "^4.5.1",
44
- "read-pkg": "^3.0.0",
45
- "rx": "2.3.24",
34
+ "lodash": "^4.17.10",
35
+ "read-pkg": "^4.0.1",
36
+ "rxjs": "^6.3.3",
46
37
  "spawn-command": "^0.0.2-1",
47
- "supports-color": "^3.2.3",
48
- "tree-kill": "^1.1.0"
38
+ "supports-color": "^4.5.0",
39
+ "tree-kill": "^1.1.0",
40
+ "yargs": "^12.0.1"
49
41
  },
50
42
  "devDependencies": {
51
- "chai": "^1.10.0",
52
- "eslint": "^4.19.1",
53
- "mocha": "^2.1.0",
54
- "mustache": "^1.0.0",
55
- "releasor": "^1.2.1",
56
- "semver": "^4.2.0",
57
- "shell-quote": "^1.4.3",
58
- "shelljs": "^0.3.0",
59
- "sinon": "^4.1.3",
60
- "string": "^3.0.0"
43
+ "coveralls": "^3.0.2",
44
+ "eslint": "^5.4.0",
45
+ "jest": "^23.5.0",
46
+ "jest-create-mock-instance": "^1.1.0"
47
+ },
48
+ "jest": {
49
+ "collectCoverage": true,
50
+ "collectCoverageFrom": [
51
+ "src/**/*.js"
52
+ ],
53
+ "coveragePathIgnorePatterns": [
54
+ "/fixtures/",
55
+ "/node_modules/"
56
+ ],
57
+ "testEnvironment": "node"
61
58
  }
62
59
  }
@@ -0,0 +1,13 @@
1
+ module.exports = class ExpandNpmShortcut {
2
+ parse(commandInfo) {
3
+ const [, npmCmd, cmdName, args] = commandInfo.command.match(/^(npm|yarn):(\S+)(.*)/) || [];
4
+ if (!cmdName) {
5
+ return commandInfo;
6
+ }
7
+
8
+ return Object.assign({}, commandInfo, {
9
+ name: commandInfo.name || cmdName,
10
+ command: `${npmCmd} run ${cmdName}${args}`
11
+ });
12
+ }
13
+ };
@@ -0,0 +1,36 @@
1
+ const ExpandNpmShortcut = require('./expand-npm-shortcut');
2
+ const parser = new ExpandNpmShortcut();
3
+
4
+ it('returns same command if no npm: prefix is present', () => {
5
+ const commandInfo = {
6
+ name: 'echo',
7
+ command: 'echo foo'
8
+ };
9
+ expect(parser.parse(commandInfo)).toBe(commandInfo);
10
+ });
11
+
12
+ for (const npmCmd of ['npm', 'yarn']) {
13
+ describe(`with ${npmCmd}: prefix`, () => {
14
+ it(`expands to "${npmCmd} run <script> <args>"`, () => {
15
+ const commandInfo = {
16
+ name: 'echo',
17
+ command: `${npmCmd}:foo -- bar`
18
+ };
19
+ expect(parser.parse(commandInfo)).toEqual({
20
+ name: 'echo',
21
+ command: `${npmCmd} run foo -- bar`
22
+ });
23
+ });
24
+
25
+ it('sets name to script name if none', () => {
26
+ const commandInfo = {
27
+ command: `${npmCmd}:foo -- bar`
28
+ };
29
+ expect(parser.parse(commandInfo)).toEqual({
30
+ name: 'foo',
31
+ command: `${npmCmd} run foo -- bar`
32
+ });
33
+ });
34
+ });
35
+
36
+ }
@@ -0,0 +1,34 @@
1
+ const _ = require('lodash');
2
+ const readPkg = require('read-pkg');
3
+
4
+ module.exports = class ExpandNpmWildcard {
5
+ constructor(readPackage = readPkg.sync) {
6
+ this.readPackage = readPackage;
7
+ }
8
+
9
+ parse(commandInfo) {
10
+ const [, npmCmd, cmdName, args] = commandInfo.command.match(/(npm|yarn) run (\S+)([^&]*)/) || [];
11
+ const wildcardPosition = (cmdName || '').indexOf('*');
12
+
13
+ // If the regex didn't match an npm script, or it has no wildcard,
14
+ // then we have nothing to do here
15
+ if (!cmdName || wildcardPosition === -1) {
16
+ return commandInfo;
17
+ }
18
+
19
+ if (!this.scripts) {
20
+ this.scripts = Object.keys(this.readPackage().scripts || {});
21
+ }
22
+
23
+ const preWildcard = _.escapeRegExp(cmdName.substr(0, wildcardPosition));
24
+ const postWildcard = _.escapeRegExp(cmdName.substr(wildcardPosition + 1));
25
+ const wildcardRegex = new RegExp(`^${preWildcard}(.*?)${postWildcard}$`);
26
+
27
+ return this.scripts
28
+ .filter(script => wildcardRegex.test(script))
29
+ .map(script => Object.assign({}, commandInfo, {
30
+ command: `${npmCmd} run ${script}${args}`,
31
+ name: script
32
+ }));
33
+ }
34
+ };
@@ -0,0 +1,58 @@
1
+ const ExpandNpmWildcard = require('./expand-npm-wildcard');
2
+
3
+ let parser, readPkg;
4
+
5
+ beforeEach(() => {
6
+ readPkg = jest.fn();
7
+ parser = new ExpandNpmWildcard(readPkg);
8
+ });
9
+
10
+ it('returns same command if not an npm run command', () => {
11
+ const commandInfo = {
12
+ command: 'npm test'
13
+ };
14
+
15
+ expect(readPkg).not.toHaveBeenCalled();
16
+ expect(parser.parse(commandInfo)).toBe(commandInfo);
17
+ });
18
+
19
+ it('returns same command if no wildcard present', () => {
20
+ const commandInfo = {
21
+ command: 'npm run foo bar'
22
+ };
23
+
24
+ expect(readPkg).not.toHaveBeenCalled();
25
+ expect(parser.parse(commandInfo)).toBe(commandInfo);
26
+ });
27
+
28
+ it('expands to nothing if no scripts exist in package.json', () => {
29
+ readPkg.mockReturnValue({});
30
+
31
+ expect(parser.parse({ command: 'npm run foo-*-baz qux' })).toEqual([]);
32
+ });
33
+
34
+ for (const npmCmd of ['npm', 'yarn']) {
35
+ describe(`with an ${npmCmd}: prefix`, () => {
36
+ it('expands to all scripts matching pattern', () => {
37
+ readPkg.mockReturnValue({
38
+ scripts: {
39
+ 'foo-bar-baz': '',
40
+ 'foo--baz': '',
41
+ }
42
+ });
43
+
44
+ expect(parser.parse({ command: `${npmCmd} run foo-*-baz qux` })).toEqual([
45
+ { name: 'foo-bar-baz', command: `${npmCmd} run foo-bar-baz qux` },
46
+ { name: 'foo--baz', command: `${npmCmd} run foo--baz qux` },
47
+ ]);
48
+ });
49
+
50
+ it('caches scripts upon calls', () => {
51
+ readPkg.mockReturnValue({});
52
+ parser.parse({ command: `${npmCmd} run foo-*-baz qux` });
53
+ parser.parse({ command: `${npmCmd} run foo-*-baz qux` });
54
+
55
+ expect(readPkg).toHaveBeenCalledTimes(1);
56
+ });
57
+ });
58
+ }