in-parallel-lit 1.0.0 → 3.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.
package/src/index.js DELETED
@@ -1,201 +0,0 @@
1
- import { InParallelError } from './lib/in-parallel-error.js';
2
- import { spawn } from './lib/spawn.js';
3
- import { MemoryWritable } from './lib/memory-writable.js';
4
- import { getSignalNumber } from './lib/get-signal-num.js';
5
- import { removeFromArr } from './lib/remove-from-arr.js';
6
- import { wrapStreamWithLabel } from './lib/wrap-stream-with-label.js';
7
- import { getStreamKind } from './lib/get-stream-kind.js';
8
-
9
- /**
10
- * prog represents the main program logic.
11
- * @param {{[key: string]: any, _: string[]}} opts
12
- * @param {NodeJS.process} proc
13
- * @returns {Promise<void>}
14
- */
15
- export function prog(opts, proc) {
16
- const { _: tasks, ...options } = opts;
17
-
18
- const customTaskNames =
19
- options.names != null ? options.names.split(',').map(n => n.trim()) : [];
20
-
21
- return new Promise((resolve, reject) => {
22
- let results = [];
23
- let queue = [];
24
- let promises = [];
25
- let error = null;
26
- let aborted = false;
27
-
28
- // NOTE(joel): Resolve if no tasks are passed.
29
- if (tasks.length === 0) return done();
30
-
31
- // NOTE(joel): Pre-build result and queue arays.
32
- for (let i = 0; i < tasks.length; i++) {
33
- results.push({ name: tasks[i], code: undefined });
34
- queue.push({ name: tasks[i], index: i });
35
- }
36
-
37
- function done() {
38
- if (error == null) return resolve(results);
39
- return reject(error);
40
- }
41
-
42
- function abort() {
43
- if (aborted) return;
44
-
45
- aborted = true;
46
- if (promises.length === 0) return done();
47
- for (const p of promises) {
48
- p.abort();
49
- }
50
- return Promise.all(promises).then(done, reject);
51
- }
52
-
53
- function next() {
54
- if (aborted) return;
55
-
56
- // NOTE(joel): Return without resolving if the queue is empty.
57
- if (queue.length === 0) {
58
- if (promises.length === 0) return done();
59
- return;
60
- }
61
-
62
- const task = queue.shift();
63
-
64
- const originalOutputStream = proc.stdout;
65
- const optionsClone = {
66
- stdout: proc.stdout,
67
- stderr: proc.stderr,
68
- stdin: proc.stdin,
69
- customName: customTaskNames[task.index],
70
- };
71
-
72
- const writer = new MemoryWritable();
73
-
74
- if (options['aggregate-output']) {
75
- optionsClone.stdout = writer;
76
- }
77
-
78
- const promise = runTask(task.name, optionsClone);
79
-
80
- promises.push(promise);
81
- promise.then(
82
- result => {
83
- removeFromArr(promises, promise);
84
-
85
- if (aborted) return;
86
-
87
- if (options['aggregate-output']) {
88
- originalOutputStream.write(writer.toString());
89
- }
90
-
91
- // NOTE(joel): Check if the task failed as a result of a signal, and
92
- // amend the exit code as a result.
93
- if (result.code === null && result.signal !== null) {
94
- // NOTE(joel): An exit caused by a signal must return a status code
95
- // of 128 plus the value of the signal code.
96
- // @see https://nodejs.org/api/process.html#process_exit_codes
97
- result.code = 128 + getSignalNumber(result.signal);
98
- }
99
-
100
- // NOTE(joel): Save the result.
101
- results[task.index].code = result.code;
102
-
103
- // NOTE(joel): Aborts all tasks if it's an error.
104
- if (result.code) {
105
- error = new InParallelError(result, results);
106
- if (!options['continue-on-error']) {
107
- return abort();
108
- }
109
- }
110
-
111
- next();
112
- },
113
- err => {
114
- removeFromArr(promises, promise);
115
- if (!options['continue-on-error']) {
116
- error = err;
117
- return abort();
118
- }
119
- next();
120
- },
121
- );
122
- }
123
-
124
- let end = tasks.length;
125
- if (
126
- typeof options['max-parallel'] === 'number' &&
127
- options['max-parallel'] > 0
128
- ) {
129
- end = Math.min(tasks.length, options['max-parallel']);
130
- }
131
-
132
- for (let i = 0; i < end; ++i) {
133
- next();
134
- }
135
- });
136
- }
137
-
138
- ////////////////////////////////////////////////////////////////////////////////
139
-
140
- /**
141
- * @typedef {Object} RunTaskOptions
142
- * @prop {stream.Readable} stdin
143
- * @prop {stream.Writable} stdout
144
- * @prop {stream.Writable} stderr
145
- * @prop {string} [customName=]
146
- */
147
-
148
- /**
149
- * runTask executes a single task as a child process.
150
- * @param {string} name
151
- * @param {RunTaskOptions} opts
152
- * @returns {Promise<{name: string, code: number, signal: string}>}
153
- */
154
- function runTask(name, opts) {
155
- let proc = null;
156
-
157
- const task = new Promise((resolve, reject) => {
158
- const stdin = opts.stdin;
159
- const stdout = wrapStreamWithLabel(opts.stdout, opts.customName || name);
160
- const stderr = wrapStreamWithLabel(opts.stderr, opts.customName || name);
161
-
162
- const stdinKind = getStreamKind(stdin, process.stdin);
163
- const stdoutKind = getStreamKind(stdout, process.stdout);
164
- const stderrKind = getStreamKind(stderr, process.stderr);
165
-
166
- const [spawnName, ...spawnArgs] = name.split(' ');
167
-
168
- proc = spawn(spawnName, spawnArgs, {
169
- stdio: [stdinKind, stdoutKind, stderrKind],
170
- });
171
-
172
- // Piping stdio.
173
- if (stdinKind === 'pipe') {
174
- stdin.pipe(proc.stdin);
175
- }
176
- if (stdoutKind === 'pipe') {
177
- proc.stdout.pipe(stdout, { end: false });
178
- }
179
- if (stderrKind === 'pipe') {
180
- proc.stderr.pipe(stderr, { end: false });
181
- }
182
-
183
- // Register
184
- proc.on('error', err => {
185
- proc = null;
186
- return reject(err);
187
- });
188
- proc.on('close', (code, signal) => {
189
- proc = null;
190
- return resolve({ name, code, signal });
191
- });
192
- });
193
-
194
- task.abort = () => {
195
- if (proc == null) return;
196
- proc.kill();
197
- proc = null;
198
- };
199
-
200
- return task;
201
- }
@@ -1,32 +0,0 @@
1
- const signals = {
2
- SIGABRT: 6,
3
- SIGALRM: 14,
4
- SIGBUS: 10,
5
- SIGCHLD: 20,
6
- SIGCONT: 19,
7
- SIGFPE: 8,
8
- SIGHUP: 1,
9
- SIGILL: 4,
10
- SIGINT: 2,
11
- SIGKILL: 9,
12
- SIGPIPE: 13,
13
- SIGQUIT: 3,
14
- SIGSEGV: 11,
15
- SIGSTOP: 17,
16
- SIGTERM: 15,
17
- SIGTRAP: 5,
18
- SIGTSTP: 18,
19
- SIGTTIN: 21,
20
- SIGTTOU: 22,
21
- SIGUSR1: 30,
22
- SIGUSR2: 31,
23
- };
24
-
25
- /**
26
- * Converts a signal name to a number.
27
- * @param {string} signal
28
- * @returns {number}
29
- */
30
- export function getSignalNumber(signal) {
31
- return signals[signal] || 0;
32
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Converts a given stream to an option for `child_process.spawn`.
3
- * @param {stream.Readable|stream.Writable|null} stream
4
- * @param {process.stdin|process.stdout|process.stderr} std
5
- * @returns {string|stream.Readable|stream.Writable}
6
- */
7
- export function getStreamKind(stream, std) {
8
- if (stream == null) return 'ignore';
9
- // NOTE(joel): `|| !std.isTTY` is needed for the workaround of
10
- // https://github.com/nodejs/node/issues/5620
11
- if (stream !== std || !std.isTTY) return 'pipe';
12
- return stream;
13
- }
@@ -1,8 +0,0 @@
1
- export class InParallelError extends Error {
2
- constructor(causeResult, allResults) {
3
- super(`"${causeResult.name}" exited with ${causeResult.code}.`);
4
- this.name = causeResult.name;
5
- this.code = causeResult.code;
6
- this.results = allResults;
7
- }
8
- }
@@ -1,110 +0,0 @@
1
- import os from 'os';
2
- import crossSpawn from 'cross-spawn';
3
-
4
- ////////////////////////////////////////////////////////////////////////////////
5
-
6
- /**
7
- * Wrap crossSpawn in a Promise.
8
- * @param {string} cmd
9
- * @param {Array<string|number>} args
10
- * @param {any} options
11
- */
12
- function crossSpawnPromise(cmd, args, options) {
13
- return new Promise((resolve, reject) => {
14
- let stdout = '';
15
- let stderr = '';
16
- const ch = crossSpawn(cmd, args, options);
17
-
18
- ch.stdout.on('data', d => {
19
- stdout += d.toString();
20
- });
21
-
22
- ch.stderr.on('data', d => {
23
- stderr += d.toString();
24
- });
25
-
26
- ch.on('error', err => reject(err));
27
-
28
- ch.on('close', code => {
29
- if (stderr) return reject(stderr);
30
- if (code !== 0) return reject(`${cmd} exited with code ${code}`);
31
- return resolve(stdout);
32
- });
33
- });
34
- }
35
-
36
- ////////////////////////////////////////////////////////////////////////////////
37
-
38
- /**
39
- * Kills a process by ID and all its subprocesses.
40
- * @param {number} pid
41
- */
42
- export async function killPids(pid, platform = process.platform) {
43
- if (platform === 'win32') {
44
- crossSpawn('taskkill', ['/F', '/T', '/PID', pid]);
45
- return;
46
- }
47
-
48
- try {
49
- // NOTE(joel): Example of stdout:
50
- // PPID PID
51
- // 1 430
52
- // 430 432
53
- // 1 727
54
- // 1 7166
55
- let stdout = await crossSpawnPromise('ps', ['-A', '-o', 'ppid,pid']);
56
- stdout = stdout.split(os.EOL);
57
-
58
- let pidExists = false;
59
- const pidTree = {};
60
- for (let i = 1; i < stdout.length; i++) {
61
- stdout[i] = stdout[i].trim();
62
- if (!stdout[i]) continue;
63
-
64
- stdout[i] = stdout[i].split(/\s+/);
65
- stdout[i][0] = parseInt(stdout[i][0], 10); // PPID
66
- stdout[i][1] = parseInt(stdout[i][1], 10); // PID
67
-
68
- // NOTE(joel): Make sure our pid is part of the `ps` output.
69
- if (
70
- (!pidExists && stdout[i][1] === pid) ||
71
- (!pidExists && stdout[i][0] === pid)
72
- ) {
73
- pidExists = true;
74
- }
75
-
76
- // NOTE(joel): Build the adiacency Hash Map (pid -> [children of pid])
77
- if (pidTree[stdout[i][0]]) {
78
- pidTree[stdout[i][0]].push(stdout[i][1]);
79
- } else {
80
- pidTree[stdout[i][0]] = [stdout[i][1]];
81
- }
82
- }
83
-
84
- // NOTE(joel): No matching pid found.
85
- if (!pidExists) return;
86
-
87
- // NOTE(joel): Starting by the `pid` provided, traverse the tree using
88
- // the adiacency Hash Map until the whole subtree is visited. Each pid
89
- // encountered while visiting is added to the pids array.
90
- let idx = 0;
91
- const pids = [pid];
92
- while (idx < pids.length) {
93
- let curpid = pids[idx++];
94
- if (!pidTree[curpid]) continue;
95
-
96
- for (let j = 0; j < pidTree[curpid].length; j++) {
97
- pids.push(pidTree[curpid][j]);
98
- }
99
-
100
- delete pidTree[curpid];
101
- }
102
-
103
- // NOTE(joel): Finally, kill all pids.
104
- for (const pid of pids) {
105
- process.kill(pid);
106
- }
107
- } catch (err) {
108
- /* Silence is golden */
109
- }
110
- }
@@ -1,69 +0,0 @@
1
- import { Writable } from 'node:stream';
2
- import { StringDecoder } from 'node:string_decoder';
3
-
4
- const noop = () => {};
5
-
6
- /**
7
- * Writable stream that can hold written chunks.
8
- */
9
- export class MemoryWritable extends Writable {
10
- queue = [];
11
- data = [];
12
-
13
- constructor(data = null) {
14
- super();
15
-
16
- // NOTE(joel): Ensure `data` is an array.
17
- this.data = Array.isArray(data) ? data : [data];
18
-
19
- this.queue = [];
20
- for (let chunk of this.data) {
21
- if (chunk == null) continue;
22
-
23
- // NOTE(joel): Ensure each chunk is a Buffer.
24
- if (!(chunk instanceof Buffer)) {
25
- chunk = Buffer.from(chunk);
26
- }
27
- this.queue.push(chunk);
28
- }
29
- }
30
-
31
- _write(chunk, enc, cb = noop) {
32
- let decoder = null;
33
- try {
34
- decoder = enc && enc !== 'buffer' ? new StringDecoder(enc) : null;
35
- } catch (err) {
36
- return cb(err);
37
- }
38
-
39
- let decodedChunk = decoder != null ? decoder.write(chunk) : chunk;
40
- this.queue.push(decodedChunk);
41
- cb();
42
- }
43
-
44
- _getQueueSize() {
45
- let size = 0;
46
- for (let i = 0; i < this.queue.length; i++) {
47
- size += this.queue[i].length;
48
- }
49
- return size;
50
- }
51
-
52
- toString() {
53
- let str = '';
54
- for (const chunk of this.queue) {
55
- str += chunk;
56
- }
57
- return str;
58
- }
59
-
60
- toBuffer() {
61
- const buffer = Buffer.alloc(this._getQueueSize());
62
- let currentOffset = 0;
63
- for (const chunk of this.queue) {
64
- chunk.copy(buffer, currentOffset);
65
- currentOffset += chunk.length;
66
- }
67
- return buffer;
68
- }
69
- }
@@ -1,33 +0,0 @@
1
- import { Transform } from 'node:stream';
2
-
3
- const ALL_BR = /\n/g;
4
-
5
- export class PrefixTransform extends Transform {
6
- constructor(prefix) {
7
- super();
8
- this.prefix = prefix;
9
- this.lastPrefix = null;
10
- this.lastIsLinebreak = true;
11
- }
12
-
13
- _transform(chunk, _enc, cb) {
14
- const firstPrefix = this.lastIsLinebreak
15
- ? this.prefix
16
- : this.lastPrefix !== this.prefix
17
- ? '\n'
18
- : '';
19
- const prefixed = `${firstPrefix}${chunk}`.replace(
20
- ALL_BR,
21
- `\n${this.prefix}`,
22
- );
23
- const index = prefixed.indexOf(
24
- this.prefix,
25
- Math.max(0, prefixed.length - this.prefix.length),
26
- );
27
-
28
- this.lastPrefix = this.prefix;
29
- this.lastIsLinebreak = index !== -1;
30
-
31
- cb(null, index !== -1 ? prefixed.slice(0, index) : prefixed);
32
- }
33
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * removeFromArr removes an `item` from `arr`.
3
- * @param {any[]} arr
4
- * @param {any} item
5
- */
6
- export function removeFromArr(arr, item) {
7
- const index = arr.indexOf(item);
8
- if (index !== -1) {
9
- arr.splice(index, 1);
10
- }
11
- }
@@ -1,67 +0,0 @@
1
- let FORCE_COLOR;
2
- let NODE_DISABLE_COLORS;
3
- let NO_COLOR;
4
- let TERM;
5
- let isTTY = true;
6
-
7
- if (typeof process !== 'undefined') {
8
- ({ FORCE_COLOR, NODE_DISABLE_COLORS, NO_COLOR, TERM } = process.env);
9
- isTTY = process.stdout && process.stdout.isTTY;
10
- }
11
-
12
- const enabled =
13
- !NODE_DISABLE_COLORS &&
14
- NO_COLOR == null &&
15
- TERM !== 'dumb' &&
16
- ((FORCE_COLOR != null && FORCE_COLOR !== '0') || isTTY);
17
-
18
- ////////////////////////////////////////////////////////////////////////////////
19
-
20
- /**
21
- * Returns functions that wrap a given string with color char codes.
22
- * @param {number} x
23
- * @param {number} y
24
- * @returns {(txt: string) => string}
25
- */
26
- function createColor(x, y) {
27
- const rgx = new RegExp(`\\x1b\\[${y}m`, 'g');
28
- const open = `\x1b[${x}m`;
29
- const close = `\x1b[${y}m`;
30
-
31
- return function (txt) {
32
- if (!enabled || txt == null) return txt;
33
- return (
34
- open +
35
- (~('' + txt).indexOf(close) ? txt.replace(rgx, close + open) : txt) +
36
- close
37
- );
38
- };
39
- }
40
-
41
- ////////////////////////////////////////////////////////////////////////////////
42
-
43
- const colors = [
44
- createColor(36, 39), // cyan
45
- createColor(32, 39), // green
46
- createColor(35, 39), // magenta
47
- createColor(33, 39), // yellow
48
- createColor(31, 39), // red
49
- ];
50
-
51
- let colorIndex = 0;
52
- const taskNamesToColors = new Map();
53
-
54
- /**
55
- * Select a color from given task name.
56
- * @param {string} taskName
57
- * @returns {(txt: string) => string}
58
- */
59
- export function selectColor(taskName) {
60
- let color = taskNamesToColors.get(taskName);
61
- if (color == null) {
62
- color = colors[colorIndex];
63
- colorIndex = (colorIndex + 1) % colors.length;
64
- taskNamesToColors.set(taskName, color);
65
- }
66
- return color;
67
- }
package/src/lib/spawn.js DELETED
@@ -1,20 +0,0 @@
1
- import crossSpawn from 'cross-spawn';
2
- import { killPids } from './kill-pids.js';
3
-
4
- /**
5
- * Launches a new process with the given command.
6
- * This is almost same as `child_process.spawn`, but it adds a `kill` method
7
- * that kills the instance process and its sub processes.
8
- * @param {string} command
9
- * @param {string[]} args
10
- * @param {object} options
11
- * @returns {ChildProcess}
12
- */
13
- export function spawn(command, args, options) {
14
- const child = crossSpawn(command, args, options);
15
- child.kill = function kill() {
16
- killPids(this.pid);
17
- };
18
-
19
- return child;
20
- }
@@ -1,20 +0,0 @@
1
- import { selectColor } from './select-color.js';
2
- import { PrefixTransform } from './prefix-transform.js';
3
-
4
- /**
5
- * Wraps stdout/stderr with a transform stream to add the task name as prefix.
6
- * @param {stream.Writable} source
7
- * @param {string} label
8
- * @returns {stream.Writable}
9
- */
10
- export function wrapStreamWithLabel(source, label) {
11
- if (source == null) return source;
12
-
13
- const color = source.isTTY ? selectColor(label) : x => x;
14
- const prefix = color(`[${label}] `);
15
- const target = new PrefixTransform(prefix);
16
-
17
- target.pipe(source);
18
-
19
- return target;
20
- }