concurrently 9.2.1 → 10.0.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 (149) hide show
  1. package/README.md +25 -14
  2. package/dist/bin/{concurrently.js → index.js} +23 -62
  3. package/dist/bin/index.spec.d.ts +1 -0
  4. package/dist/bin/index.spec.js +368 -0
  5. package/dist/bin/normalize-cli-command.d.ts +1 -0
  6. package/dist/bin/normalize-cli-command.js +15 -0
  7. package/dist/bin/normalize-cli-command.spec.d.ts +1 -0
  8. package/dist/bin/normalize-cli-command.spec.js +36 -0
  9. package/dist/bin/read-package-json.d.ts +4 -0
  10. package/dist/bin/read-package-json.js +17 -0
  11. package/dist/lib/__fixtures__/create-mock-instance.d.ts +2 -0
  12. package/dist/lib/__fixtures__/create-mock-instance.js +5 -0
  13. package/dist/lib/__fixtures__/fake-command.d.ts +6 -0
  14. package/dist/lib/__fixtures__/fake-command.js +37 -0
  15. package/dist/lib/assert.d.ts +10 -0
  16. package/dist/lib/assert.js +24 -0
  17. package/dist/lib/assert.spec.d.ts +1 -0
  18. package/dist/lib/assert.spec.js +41 -0
  19. package/dist/{src → lib}/command-parser/expand-arguments.d.ts +2 -2
  20. package/dist/{src → lib}/command-parser/expand-arguments.js +7 -12
  21. package/dist/lib/command-parser/expand-arguments.spec.d.ts +1 -0
  22. package/dist/lib/command-parser/expand-arguments.spec.js +57 -0
  23. package/dist/{src → lib}/command-parser/expand-shortcut.d.ts +2 -2
  24. package/dist/{src → lib}/command-parser/expand-shortcut.js +1 -5
  25. package/dist/lib/command-parser/expand-shortcut.spec.d.ts +1 -0
  26. package/dist/lib/command-parser/expand-shortcut.spec.js +36 -0
  27. package/dist/{src → lib}/command-parser/expand-wildcard.d.ts +2 -2
  28. package/dist/{src → lib}/command-parser/expand-wildcard.js +21 -28
  29. package/dist/lib/command-parser/expand-wildcard.spec.d.ts +1 -0
  30. package/dist/lib/command-parser/expand-wildcard.spec.js +288 -0
  31. package/dist/{src → lib}/command.d.ts +5 -4
  32. package/dist/{src → lib}/command.js +3 -39
  33. package/dist/lib/command.spec.d.ts +1 -0
  34. package/dist/lib/command.spec.js +369 -0
  35. package/dist/{src → lib}/completion-listener.d.ts +2 -2
  36. package/dist/{src → lib}/completion-listener.js +9 -46
  37. package/dist/lib/completion-listener.spec.d.ts +1 -0
  38. package/dist/lib/completion-listener.spec.js +229 -0
  39. package/dist/{src → lib}/concurrently.d.ts +10 -10
  40. package/dist/{src → lib}/concurrently.js +29 -40
  41. package/dist/lib/concurrently.spec.d.ts +1 -0
  42. package/dist/lib/concurrently.spec.js +320 -0
  43. package/dist/{src → lib}/date-format.d.ts +2 -2
  44. package/dist/{src → lib}/date-format.js +97 -76
  45. package/dist/lib/date-format.spec.d.ts +1 -0
  46. package/dist/lib/date-format.spec.js +480 -0
  47. package/dist/{src → lib}/defaults.d.ts +2 -6
  48. package/dist/{src → lib}/defaults.js +16 -23
  49. package/dist/{src → lib}/flow-control/input-handler.d.ts +4 -4
  50. package/dist/{src → lib}/flow-control/input-handler.js +8 -44
  51. package/dist/lib/flow-control/input-handler.spec.d.ts +1 -0
  52. package/dist/lib/flow-control/input-handler.spec.js +116 -0
  53. package/dist/{src → lib}/flow-control/kill-on-signal.d.ts +3 -3
  54. package/dist/{src → lib}/flow-control/kill-on-signal.js +3 -7
  55. package/dist/lib/flow-control/kill-on-signal.spec.d.ts +1 -0
  56. package/dist/lib/flow-control/kill-on-signal.spec.js +63 -0
  57. package/dist/{src → lib}/flow-control/kill-others.d.ts +3 -3
  58. package/dist/{src → lib}/flow-control/kill-others.js +8 -12
  59. package/dist/lib/flow-control/kill-others.spec.d.ts +1 -0
  60. package/dist/lib/flow-control/kill-others.spec.js +98 -0
  61. package/dist/{src → lib}/flow-control/log-error.d.ts +3 -3
  62. package/dist/{src → lib}/flow-control/log-error.js +1 -5
  63. package/dist/lib/flow-control/log-error.spec.d.ts +1 -0
  64. package/dist/lib/flow-control/log-error.spec.js +33 -0
  65. package/dist/{src → lib}/flow-control/log-exit.d.ts +3 -3
  66. package/dist/{src → lib}/flow-control/log-exit.js +1 -5
  67. package/dist/lib/flow-control/log-exit.spec.d.ts +1 -0
  68. package/dist/lib/flow-control/log-exit.spec.js +24 -0
  69. package/dist/{src → lib}/flow-control/log-output.d.ts +3 -3
  70. package/dist/{src → lib}/flow-control/log-output.js +1 -5
  71. package/dist/lib/flow-control/log-output.spec.d.ts +1 -0
  72. package/dist/lib/flow-control/log-output.spec.js +33 -0
  73. package/dist/{src → lib}/flow-control/log-timings.d.ts +3 -3
  74. package/dist/{src → lib}/flow-control/log-timings.js +8 -45
  75. package/dist/lib/flow-control/log-timings.spec.d.ts +1 -0
  76. package/dist/lib/flow-control/log-timings.spec.js +89 -0
  77. package/dist/{src → lib}/flow-control/logger-padding.d.ts +3 -3
  78. package/dist/{src → lib}/flow-control/logger-padding.js +7 -7
  79. package/dist/lib/flow-control/logger-padding.spec.d.ts +1 -0
  80. package/dist/lib/flow-control/logger-padding.spec.js +60 -0
  81. package/dist/{src → lib}/flow-control/output-error-handler.d.ts +4 -4
  82. package/dist/{src → lib}/flow-control/output-error-handler.js +3 -7
  83. package/dist/lib/flow-control/output-error-handler.spec.d.ts +1 -0
  84. package/dist/lib/flow-control/output-error-handler.spec.js +41 -0
  85. package/dist/{src → lib}/flow-control/restart-process.d.ts +4 -4
  86. package/dist/lib/flow-control/restart-process.js +61 -0
  87. package/dist/lib/flow-control/restart-process.spec.d.ts +1 -0
  88. package/dist/lib/flow-control/restart-process.spec.js +127 -0
  89. package/dist/{src → lib}/flow-control/teardown.d.ts +4 -5
  90. package/dist/lib/flow-control/teardown.js +45 -0
  91. package/dist/lib/flow-control/teardown.spec.d.ts +1 -0
  92. package/dist/lib/flow-control/teardown.spec.js +93 -0
  93. package/dist/{src → lib}/index.d.ts +21 -19
  94. package/dist/lib/index.js +98 -0
  95. package/dist/{src → lib}/jsonc.js +3 -8
  96. package/dist/lib/jsonc.spec.d.ts +1 -0
  97. package/dist/lib/jsonc.spec.js +73 -0
  98. package/dist/{src → lib}/logger.d.ts +3 -2
  99. package/dist/{src → lib}/logger.js +109 -65
  100. package/dist/lib/logger.spec.d.ts +1 -0
  101. package/dist/lib/logger.spec.js +507 -0
  102. package/dist/{src → lib}/observables.d.ts +1 -1
  103. package/dist/{src → lib}/observables.js +3 -6
  104. package/dist/lib/observables.spec.d.ts +1 -0
  105. package/dist/lib/observables.spec.js +29 -0
  106. package/dist/{src → lib}/output-writer.d.ts +3 -3
  107. package/dist/{src → lib}/output-writer.js +4 -41
  108. package/dist/lib/output-writer.spec.d.ts +1 -0
  109. package/dist/lib/output-writer.spec.js +96 -0
  110. package/dist/lib/prefix-color-selector.d.ts +21 -0
  111. package/dist/{src → lib}/prefix-color-selector.js +14 -31
  112. package/dist/lib/prefix-color-selector.spec.d.ts +1 -0
  113. package/dist/lib/prefix-color-selector.spec.js +159 -0
  114. package/dist/{src → lib}/spawn.d.ts +14 -5
  115. package/dist/lib/spawn.js +105 -0
  116. package/dist/lib/spawn.spec.d.ts +1 -0
  117. package/dist/lib/spawn.spec.js +100 -0
  118. package/dist/lib/utils.d.ts +25 -0
  119. package/dist/lib/utils.js +52 -0
  120. package/dist/lib/utils.spec.d.ts +1 -0
  121. package/dist/lib/utils.spec.js +58 -0
  122. package/dist/tsconfig.tsbuildinfo +1 -1
  123. package/docs/README.md +6 -0
  124. package/docs/cli/prefixing.md +44 -4
  125. package/docs/shell-resolution.md +48 -0
  126. package/package.json +59 -75
  127. package/dist/bin/read-package.d.ts +0 -6
  128. package/dist/bin/read-package.js +0 -56
  129. package/dist/src/assert.d.ts +0 -5
  130. package/dist/src/assert.js +0 -15
  131. package/dist/src/command-parser/command-parser.d.ts +0 -19
  132. package/dist/src/command-parser/command-parser.js +0 -2
  133. package/dist/src/command-parser/strip-quotes.d.ts +0 -16
  134. package/dist/src/command-parser/strip-quotes.js +0 -17
  135. package/dist/src/flow-control/flow-controller.d.ts +0 -13
  136. package/dist/src/flow-control/flow-controller.js +0 -2
  137. package/dist/src/flow-control/restart-process.js +0 -98
  138. package/dist/src/flow-control/teardown.js +0 -82
  139. package/dist/src/index.js +0 -96
  140. package/dist/src/prefix-color-selector.d.ts +0 -11
  141. package/dist/src/spawn.js +0 -49
  142. package/dist/src/utils.d.ts +0 -10
  143. package/dist/src/utils.js +0 -16
  144. package/index.d.mts +0 -7
  145. package/index.d.ts +0 -11
  146. package/index.js +0 -14
  147. package/index.mjs +0 -9
  148. /package/dist/bin/{concurrently.d.ts → index.d.ts} +0 -0
  149. /package/dist/{src → lib}/jsonc.d.ts +0 -0
@@ -0,0 +1,320 @@
1
+ import os from 'node:os';
2
+ import { Writable } from 'node:stream';
3
+ import { beforeEach, expect, it, vi } from 'vitest';
4
+ import { createMockInstance } from './__fixtures__/create-mock-instance.js';
5
+ import { createFakeProcess, FakeCommand } from './__fixtures__/fake-command.js';
6
+ import { concurrently } from './concurrently.js';
7
+ import { Logger } from './logger.js';
8
+ let spawn;
9
+ let kill;
10
+ let onFinishHooks;
11
+ let controllers;
12
+ let processes;
13
+ const create = (commands, options = {}) => concurrently(commands, Object.assign(options, { controllers, spawn, kill }));
14
+ beforeEach(() => {
15
+ vi.resetAllMocks();
16
+ processes = [];
17
+ spawn = vi.fn(() => {
18
+ const process = createFakeProcess(processes.length);
19
+ processes.push(process);
20
+ return process;
21
+ });
22
+ kill = vi.fn();
23
+ onFinishHooks = [vi.fn(), vi.fn()];
24
+ controllers = [
25
+ { handle: vi.fn((commands) => ({ commands, onFinish: onFinishHooks[0] })) },
26
+ { handle: vi.fn((commands) => ({ commands, onFinish: onFinishHooks[1] })) },
27
+ ];
28
+ });
29
+ it('fails if commands is not an array', () => {
30
+ const bomb = () => create('foo');
31
+ expect(bomb).toThrow();
32
+ });
33
+ it('fails if no commands were provided', () => {
34
+ const bomb = () => create([]);
35
+ expect(bomb).toThrow();
36
+ });
37
+ it('spawns all commands', () => {
38
+ create(['echo', 'kill']);
39
+ expect(spawn).toHaveBeenCalledTimes(2);
40
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({}));
41
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({}));
42
+ });
43
+ it('log output is passed to output stream if logger is specified in options', () => {
44
+ const logger = new Logger({ hide: [] });
45
+ const outputStream = createMockInstance(Writable);
46
+ create(['foo'], { logger, outputStream });
47
+ logger.log('foo', 'bar');
48
+ expect(outputStream.write).toHaveBeenCalledTimes(2);
49
+ expect(outputStream.write).toHaveBeenCalledWith('foo');
50
+ expect(outputStream.write).toHaveBeenCalledWith('bar');
51
+ });
52
+ it('log output is not passed to output stream after it has errored', () => {
53
+ const logger = new Logger({ hide: [] });
54
+ const outputStream = new Writable();
55
+ vi.spyOn(outputStream, 'write');
56
+ create(['foo'], { logger, outputStream });
57
+ outputStream.emit('error', new Error('test'));
58
+ logger.log('foo', 'bar');
59
+ expect(outputStream.write).not.toHaveBeenCalled();
60
+ });
61
+ it('spawns commands up to configured limit at once', () => {
62
+ create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: 2 });
63
+ expect(spawn).toHaveBeenCalledTimes(2);
64
+ expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));
65
+ expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));
66
+ // Test out of order completion picking up new processes in-order
67
+ processes[1].emit('close', 1, null);
68
+ expect(spawn).toHaveBeenCalledTimes(3);
69
+ expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));
70
+ processes[0].emit('close', null, 'SIGINT');
71
+ expect(spawn).toHaveBeenCalledTimes(4);
72
+ expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));
73
+ // Shouldn't attempt to spawn anything else.
74
+ processes[2].emit('close', 1, null);
75
+ expect(spawn).toHaveBeenCalledTimes(4);
76
+ });
77
+ it('spawns commands up to percent based limit at once', () => {
78
+ // Mock architecture with 4 cores
79
+ const cpusSpy = vi.spyOn(os, 'cpus');
80
+ cpusSpy.mockReturnValue(Array.from({ length: 4 }).fill({
81
+ model: 'Intel',
82
+ speed: 0,
83
+ times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 },
84
+ }));
85
+ create(['foo', 'bar', 'baz', 'qux'], { maxProcesses: '50%' });
86
+ // Max parallel processes should be 2 (50% of 4 cores)
87
+ expect(spawn).toHaveBeenCalledTimes(2);
88
+ expect(spawn).toHaveBeenCalledWith('foo', expect.objectContaining({}));
89
+ expect(spawn).toHaveBeenCalledWith('bar', expect.objectContaining({}));
90
+ // Close first process and expect third to be spawned
91
+ processes[0].emit('close', 1, null);
92
+ expect(spawn).toHaveBeenCalledTimes(3);
93
+ expect(spawn).toHaveBeenCalledWith('baz', expect.objectContaining({}));
94
+ // Close second process and expect fourth to be spawned
95
+ processes[1].emit('close', 1, null);
96
+ expect(spawn).toHaveBeenCalledTimes(4);
97
+ expect(spawn).toHaveBeenCalledWith('qux', expect.objectContaining({}));
98
+ });
99
+ it('does not spawn further commands on abort signal aborted', () => {
100
+ const abortController = new AbortController();
101
+ create(['foo', 'bar'], { maxProcesses: 1, abortSignal: abortController.signal });
102
+ expect(spawn).toHaveBeenCalledTimes(1);
103
+ abortController.abort();
104
+ processes[0].emit('close', 0, null);
105
+ expect(spawn).toHaveBeenCalledTimes(1);
106
+ });
107
+ it('preserves quotes in well-formed shell commands in the library API', () => {
108
+ create(['"/usr/local/bin/mytool" --flag "some value"']);
109
+ controllers.forEach((controller) => {
110
+ expect(controller.handle).toHaveBeenCalledWith([
111
+ expect.objectContaining({
112
+ command: '"/usr/local/bin/mytool" --flag "some value"',
113
+ index: 0,
114
+ }),
115
+ ]);
116
+ });
117
+ });
118
+ it('passes commands with multiple quote sets through unchanged in the library API', () => {
119
+ create(['"/usr/local/bin/mytool" --flag "some value" --other "last arg"']);
120
+ controllers.forEach((controller) => {
121
+ expect(controller.handle).toHaveBeenCalledWith([
122
+ expect.objectContaining({
123
+ command: '"/usr/local/bin/mytool" --flag "some value" --other "last arg"',
124
+ index: 0,
125
+ }),
126
+ ]);
127
+ });
128
+ });
129
+ it('runs commands with a name or prefix color', () => {
130
+ create([{ command: 'echo', prefixColor: 'red', name: 'foo' }, 'kill']);
131
+ controllers.forEach((controller) => {
132
+ expect(controller.handle).toHaveBeenCalledWith([
133
+ expect.objectContaining({ command: 'echo', index: 0, name: 'foo', prefixColor: 'red' }),
134
+ expect.objectContaining({ command: 'kill', index: 1, name: '', prefixColor: '' }),
135
+ ]);
136
+ });
137
+ });
138
+ it('runs commands with a list of colors', () => {
139
+ create(['echo', 'kill'], {
140
+ prefixColors: ['red'],
141
+ });
142
+ controllers.forEach((controller) => {
143
+ expect(controller.handle).toHaveBeenCalledWith([
144
+ expect.objectContaining({ command: 'echo', prefixColor: 'red' }),
145
+ expect.objectContaining({ command: 'kill', prefixColor: 'red' }),
146
+ ]);
147
+ });
148
+ });
149
+ it('passes commands wrapped from a controller to the next one', () => {
150
+ const fakeCommand = new FakeCommand('banana', 'banana');
151
+ controllers[0].handle.mockReturnValue({ commands: [fakeCommand] });
152
+ create(['echo']);
153
+ expect(controllers[0].handle).toHaveBeenCalledWith([
154
+ expect.objectContaining({ command: 'echo', index: 0 }),
155
+ ]);
156
+ expect(controllers[1].handle).toHaveBeenCalledWith([fakeCommand]);
157
+ expect(fakeCommand.start).toHaveBeenCalledTimes(1);
158
+ });
159
+ it('merges extra env vars into each command', () => {
160
+ create([
161
+ { command: 'echo', env: { foo: 'bar' } },
162
+ { command: 'echo', env: { foo: 'baz' } },
163
+ 'kill',
164
+ ]);
165
+ expect(spawn).toHaveBeenCalledTimes(3);
166
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
167
+ env: expect.objectContaining({ foo: 'bar' }),
168
+ }));
169
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
170
+ env: expect.objectContaining({ foo: 'baz' }),
171
+ }));
172
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
173
+ env: expect.not.objectContaining({ foo: expect.anything() }),
174
+ }));
175
+ });
176
+ it('uses cwd from options for each command', () => {
177
+ create([
178
+ { command: 'echo', env: { foo: 'bar' } },
179
+ { command: 'echo', env: { foo: 'baz' } },
180
+ 'kill',
181
+ ], {
182
+ cwd: 'foobar',
183
+ });
184
+ expect(spawn).toHaveBeenCalledTimes(3);
185
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
186
+ env: expect.objectContaining({ foo: 'bar' }),
187
+ cwd: 'foobar',
188
+ }));
189
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
190
+ env: expect.objectContaining({ foo: 'baz' }),
191
+ cwd: 'foobar',
192
+ }));
193
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
194
+ env: expect.not.objectContaining({ foo: expect.anything() }),
195
+ cwd: 'foobar',
196
+ }));
197
+ });
198
+ it('uses overridden cwd option for each command if specified', () => {
199
+ create([
200
+ { command: 'echo', env: { foo: 'bar' }, cwd: 'baz' },
201
+ { command: 'echo', env: { foo: 'baz' } },
202
+ ], {
203
+ cwd: 'foobar',
204
+ });
205
+ expect(spawn).toHaveBeenCalledTimes(2);
206
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
207
+ env: expect.objectContaining({ foo: 'bar' }),
208
+ cwd: 'baz',
209
+ }));
210
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
211
+ env: expect.objectContaining({ foo: 'baz' }),
212
+ cwd: 'foobar',
213
+ }));
214
+ });
215
+ it('uses raw from options for each command', () => {
216
+ create([{ command: 'echo' }, 'kill'], {
217
+ raw: true,
218
+ });
219
+ expect(spawn).toHaveBeenCalledTimes(2);
220
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
221
+ stdio: ['inherit', 'inherit', 'inherit'],
222
+ }));
223
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
224
+ stdio: ['inherit', 'inherit', 'inherit'],
225
+ }));
226
+ });
227
+ it('uses overridden raw option for each command if specified', () => {
228
+ create([{ command: 'echo', raw: false }, { command: 'echo' }], {
229
+ raw: true,
230
+ });
231
+ expect(spawn).toHaveBeenCalledTimes(2);
232
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
233
+ stdio: ['pipe', 'pipe', 'pipe'],
234
+ }));
235
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
236
+ stdio: ['inherit', 'inherit', 'inherit'],
237
+ }));
238
+ });
239
+ it('uses hide from options for each command', () => {
240
+ create([{ command: 'echo' }, 'kill'], {
241
+ hide: [1],
242
+ });
243
+ expect(spawn).toHaveBeenCalledTimes(2);
244
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
245
+ stdio: ['pipe', 'pipe', 'pipe'],
246
+ }));
247
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
248
+ stdio: ['pipe', 'ignore', 'ignore'],
249
+ }));
250
+ });
251
+ it('hides output for commands even if raw option is on', () => {
252
+ create([{ command: 'echo' }, 'kill'], {
253
+ hide: [1],
254
+ raw: true,
255
+ });
256
+ expect(spawn).toHaveBeenCalledTimes(2);
257
+ expect(spawn).toHaveBeenCalledWith('echo', expect.objectContaining({
258
+ stdio: ['inherit', 'inherit', 'inherit'],
259
+ }));
260
+ expect(spawn).toHaveBeenCalledWith('kill', expect.objectContaining({
261
+ stdio: ['pipe', 'ignore', 'ignore'],
262
+ }));
263
+ });
264
+ it('argument placeholders are properly replaced when additional arguments are passed', () => {
265
+ create([
266
+ { command: 'echo {1}' },
267
+ { command: 'echo {@}' },
268
+ { command: 'echo {*}' },
269
+ { command: 'echo \\{@}' },
270
+ ], {
271
+ additionalArguments: ['foo', 'bar'],
272
+ });
273
+ expect(spawn).toHaveBeenCalledTimes(4);
274
+ expect(spawn).toHaveBeenCalledWith('echo foo', expect.objectContaining({}));
275
+ expect(spawn).toHaveBeenCalledWith('echo foo bar', expect.objectContaining({}));
276
+ expect(spawn).toHaveBeenCalledWith("echo 'foo bar'", expect.objectContaining({}));
277
+ expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));
278
+ });
279
+ it('argument placeholders are not replaced when additional arguments are not defined', () => {
280
+ create([
281
+ { command: 'echo {1}' },
282
+ { command: 'echo {@}' },
283
+ { command: 'echo {*}' },
284
+ { command: 'echo \\{@}' },
285
+ ]);
286
+ expect(spawn).toHaveBeenCalledTimes(4);
287
+ expect(spawn).toHaveBeenCalledWith('echo {1}', expect.objectContaining({}));
288
+ expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));
289
+ expect(spawn).toHaveBeenCalledWith('echo {*}', expect.objectContaining({}));
290
+ expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({}));
291
+ });
292
+ it('runs onFinish hook after all commands run', async () => {
293
+ const promise = create(['foo', 'bar'], { maxProcesses: 1 });
294
+ expect(spawn).toHaveBeenCalledTimes(1);
295
+ expect(onFinishHooks[0]).not.toHaveBeenCalled();
296
+ expect(onFinishHooks[1]).not.toHaveBeenCalled();
297
+ processes[0].emit('close', 0, null);
298
+ expect(spawn).toHaveBeenCalledTimes(2);
299
+ expect(onFinishHooks[0]).not.toHaveBeenCalled();
300
+ expect(onFinishHooks[1]).not.toHaveBeenCalled();
301
+ processes[1].emit('close', 0, null);
302
+ await promise.result;
303
+ expect(onFinishHooks[0]).toHaveBeenCalled();
304
+ expect(onFinishHooks[1]).toHaveBeenCalled();
305
+ });
306
+ // This test should time out if broken
307
+ it('waits for onFinish hooks to complete before resolving', async () => {
308
+ onFinishHooks[0].mockResolvedValue(undefined);
309
+ const { result } = create(['foo', 'bar']);
310
+ processes[0].emit('close', 0, null);
311
+ processes[1].emit('close', 0, null);
312
+ await expect(result).resolves.toBeDefined();
313
+ });
314
+ it('rejects if onFinish hooks reject', async () => {
315
+ onFinishHooks[0].mockRejectedValue('error');
316
+ const { result } = create(['foo', 'bar']);
317
+ processes[0].emit('close', 0, null);
318
+ processes[1].emit('close', 0, null);
319
+ await expect(result).rejects.toBe('error');
320
+ });
@@ -1,7 +1,7 @@
1
- export type FormatterOptions = {
1
+ export interface FormatterOptions {
2
2
  locale?: string;
3
3
  calendar?: string;
4
- };
4
+ }
5
5
  /**
6
6
  * Unicode-compliant date/time formatter.
7
7
  *
@@ -1,76 +1,3 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DateFormatter = void 0;
4
- /**
5
- * Unicode-compliant date/time formatter.
6
- *
7
- * @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
8
- */
9
- class DateFormatter {
10
- options;
11
- static tokenRegex = /[A-Z]/i;
12
- parts = [];
13
- constructor(pattern, options = {}) {
14
- this.options = options;
15
- let i = 0;
16
- while (i < pattern.length) {
17
- const char = pattern[i];
18
- const { fn, length } = char === "'"
19
- ? this.compileLiteral(pattern, i)
20
- : DateFormatter.tokenRegex.test(char)
21
- ? this.compileToken(pattern, i)
22
- : this.compileOther(pattern, i);
23
- this.parts.push(fn);
24
- i += length;
25
- }
26
- }
27
- compileLiteral(pattern, offset) {
28
- let length = 1;
29
- let value = '';
30
- for (; length < pattern.length; length++) {
31
- const i = offset + length;
32
- const char = pattern[i];
33
- if (char === "'") {
34
- const nextChar = pattern[i + 1];
35
- length++;
36
- // if the next character is another single quote, it's been escaped.
37
- // if not, then the literal has been closed
38
- if (nextChar !== "'") {
39
- break;
40
- }
41
- }
42
- value += char;
43
- }
44
- return { fn: () => value || "'", length };
45
- }
46
- compileOther(pattern, offset) {
47
- let value = '';
48
- while (!DateFormatter.tokenRegex.test(pattern[offset]) && pattern[offset] !== "'") {
49
- value += pattern[offset++];
50
- }
51
- return { fn: () => value, length: value.length };
52
- }
53
- compileToken(pattern, offset) {
54
- const type = pattern[offset];
55
- const token = tokens.get(type);
56
- if (!token) {
57
- throw new SyntaxError(`Formatting token "${type}" is invalid`);
58
- }
59
- let length = 0;
60
- while (pattern[offset + length] === type) {
61
- length++;
62
- }
63
- const tokenFn = token[length - 1];
64
- if (!tokenFn) {
65
- throw new RangeError(`Formatting token "${type.repeat(length)}" is unsupported`);
66
- }
67
- return { fn: tokenFn, length };
68
- }
69
- format(date) {
70
- return this.parts.reduce((output, part) => output + String(part(date, this.options)), '');
71
- }
72
- }
73
- exports.DateFormatter = DateFormatter;
74
1
  /**
75
2
  * A map of token to its implementations by length.
76
3
  * If an index is undefined, then that token length is unsupported.
@@ -241,6 +168,99 @@ function getLocale(options) {
241
168
  }
242
169
  return locale;
243
170
  }
171
+ /**
172
+ * Unicode-compliant date/time formatter.
173
+ *
174
+ * @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
175
+ */
176
+ export class DateFormatter {
177
+ options;
178
+ static tokenRegex = /[A-Z]/i;
179
+ parts = [];
180
+ constructor(pattern, options = {}) {
181
+ this.options = options;
182
+ let i = 0;
183
+ while (i < pattern.length) {
184
+ const char = pattern[i];
185
+ const { fn, length } = char === "'"
186
+ ? this.compileLiteral(pattern, i)
187
+ : DateFormatter.tokenRegex.test(char)
188
+ ? this.compileToken(pattern, i)
189
+ : this.compileOther(pattern, i);
190
+ this.parts.push(fn);
191
+ i += length;
192
+ }
193
+ }
194
+ compileLiteral(pattern, offset) {
195
+ let length = 1;
196
+ let value = '';
197
+ for (; length < pattern.length; length++) {
198
+ const i = offset + length;
199
+ const char = pattern[i];
200
+ if (char === "'") {
201
+ const nextChar = pattern[i + 1];
202
+ length++;
203
+ // if the next character is another single quote, it's been escaped.
204
+ // if not, then the literal has been closed
205
+ if (nextChar !== "'") {
206
+ break;
207
+ }
208
+ }
209
+ value += char;
210
+ }
211
+ return { fn: () => value || "'", length };
212
+ }
213
+ compileOther(pattern, offset) {
214
+ let value = '';
215
+ while (!DateFormatter.tokenRegex.test(pattern[offset]) && pattern[offset] !== "'") {
216
+ value += pattern[offset++];
217
+ }
218
+ return { fn: () => value, length: value.length };
219
+ }
220
+ compileToken(pattern, offset) {
221
+ const type = pattern[offset];
222
+ const token = tokens.get(type);
223
+ if (!token) {
224
+ throw new SyntaxError(`Formatting token "${type}" is invalid`);
225
+ }
226
+ let length = 0;
227
+ while (pattern[offset + length] === type) {
228
+ length++;
229
+ }
230
+ const tokenFn = token[length - 1];
231
+ if (!tokenFn) {
232
+ throw new RangeError(`Formatting token "${type.repeat(length)}" is unsupported`);
233
+ }
234
+ return { fn: tokenFn, length };
235
+ }
236
+ format(date) {
237
+ return this.parts.reduce((output, part) => output + String(part(date, this.options)), '');
238
+ }
239
+ }
240
+ /**
241
+ * See https://github.com/moment/luxon/issues/1693
242
+ *
243
+ * @todo Replace once there's a suitable alternative implementation
244
+ */
245
+ const fallbackWeekInfo = {
246
+ minimalDays: 4,
247
+ };
248
+ const weekInfoCache = new Map();
249
+ function getWeekInfo(locale) {
250
+ let data = weekInfoCache.get(locale.baseName);
251
+ if (!data) {
252
+ // The specs now envisage a method for this,
253
+ // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/getWeekInfo
254
+ data = locale.getWeekInfo?.() ?? locale.weekInfo;
255
+ // The specs have dropped minimalDays,
256
+ // see https://github.com/tc39/proposal-intl-locale-info/issues/86
257
+ if (!data.minimalDays) {
258
+ data = { ...fallbackWeekInfo, ...data };
259
+ }
260
+ weekInfoCache.set(locale.baseName, data);
261
+ }
262
+ return data;
263
+ }
244
264
  /**
245
265
  * Creates a token formatting function that returns the value of the chosen part type,
246
266
  * using the current locale's settings.
@@ -269,7 +289,8 @@ function makeTokenFn(options, type, fallback) {
269
289
  }
270
290
  function startOfWeek(date, options) {
271
291
  const locale = getLocale(options);
272
- const firstDay = locale.weekInfo.firstDay === 7 ? 0 : locale.weekInfo.firstDay;
292
+ const weekInfo = getWeekInfo(locale);
293
+ const firstDay = weekInfo.firstDay === 7 ? 0 : weekInfo.firstDay;
273
294
  const day = date.getDay();
274
295
  const diff = (day < firstDay ? 7 : 0) + day - firstDay;
275
296
  date.setDate(date.getDate() - diff);
@@ -278,7 +299,7 @@ function startOfWeek(date, options) {
278
299
  }
279
300
  function getWeekYear(date, options) {
280
301
  const locale = getLocale(options);
281
- const minimalDays = locale.weekInfo.minimalDays;
302
+ const minimalDays = getWeekInfo(locale).minimalDays;
282
303
  const year = date.getFullYear();
283
304
  const thisYear = startOfWeek(new Date(year, 0, minimalDays), options);
284
305
  const nextYear = startOfWeek(new Date(year + 1, 0, minimalDays), options);
@@ -296,7 +317,7 @@ function getWeek(date, options) {
296
317
  const locale = getLocale(options);
297
318
  const weekMs = 7 * 24 * 3600 * 1000;
298
319
  const temp = startOfWeek(new Date(date), options);
299
- const thisYear = new Date(getWeekYear(date, options), 0, locale.weekInfo.minimalDays);
320
+ const thisYear = new Date(getWeekYear(date, options), 0, getWeekInfo(locale).minimalDays);
300
321
  startOfWeek(thisYear, options);
301
322
  const diff = temp.getTime() - thisYear.getTime();
302
323
  return Math.round(diff / weekMs) + 1;
@@ -0,0 +1 @@
1
+ export {};