//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
import { Writable } from 'node:stream';
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Remove the given value from the array.
*/
const remove = <T>(array: T[], x: T): void => {
const index = array.indexOf(x);
if (index !== -1) {
array.splice(index, 1);
}
};
const signals: Record<string, number> = {
// Signal name mappings to their respective standard numeric codes.
// See: https://man7.org/linux/man-pages/man7/signal.7.html
SIGABRT: 6, // Abort signal from abort(3)
SIGALRM: 14, // Timer signal from alarm(2)
SIGBUS: 10, // Bus error (bad memory access)
SIGCHLD: 20, // Child stopped or terminated
SIGCONT: 19, // Continue if stopped
SIGFPE: 8, // Floating point exception
SIGHUP: 1, // Hangup detected on controlling terminal or death of controlling process
SIGILL: 4, // Illegal Instruction
SIGINT: 2, // Interrupt from keyboard (Ctrl+C)
SIGKILL: 9, // Kill signal (cannot be caught or ignored)
SIGPIPE: 13, // Broken pipe: write to pipe with no readers
SIGQUIT: 3, // Quit from keyboard (Ctrl+\)
SIGSEGV: 11, // Invalid memory reference (segmentation fault)
SIGSTOP: 17, // Stop process (cannot be caught or ignored)
SIGTERM: 15, // Termination signal
SIGTRAP: 5, // Trace/breakpoint trap
SIGTSTP: 18, // Stop typed at tty (Ctrl+Z)
SIGTTIN: 21, // tty input for background process
SIGTTOU: 22, // tty output for background process
SIGUSR1: 30, // User-defined signal 1
SIGUSR2: 31, // User-defined signal 2
};
/**
* Converts a signal name to a number.
*/
const convert = (signal: string): number => {
return signals[signal] || 0;
};
/**
* Simple in-memory writable stream
*/
class MemoryStream extends Writable {
private chunks: Buffer[] = [];
_write(
chunk: Buffer,
_encoding: string,
callback: (error?: Error | null) => void
): void {
this.chunks.push(chunk);
callback();
}
toString(): string {
return Buffer.concat(this.chunks).toString('utf8');
}
}
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
interface TaskResult {
name: string;
code: number | null;
signal?: string | null;
}
interface TaskQueueItem {
name: string;
index: number;
}
interface RunTaskOptions {
stdout: NodeJS.WritableStream;
stderr?: NodeJS.WritableStream;
aggregateOutput?: boolean;
continueOnError?: boolean;
race?: boolean;
maxParallel?: number;
}
interface TaskPromise extends Promise<TaskResult> {
abort?: () => void;
}
//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------
/**
* Run npm-scripts of given names in parallel.
*
* If a npm-script exited with a non-zero code, this aborts other all npm-scripts.
*
* Note: This is a simplified version for our use case.
* The full implementation would require the actual runTask function from npm-run-all.
*/
export const runTasks = (
tasks: string[],
options: RunTaskOptions
): Promise<TaskResult[]> => {
return new Promise((resolve, reject) => {
if (tasks.length === 0) {
resolve([]);
return;
}
const results: TaskResult[] = tasks.map((task) => ({
name: task,
code: undefined as any,
}));
const queue: TaskQueueItem[] = tasks.map((task, index) => ({
name: task,
index,
}));
const promises: TaskPromise[] = [];
let error: Error | null = null;
let aborted = false;
/**
* Done.
*/
const done = (): void => {
if (error == null) {
resolve(results);
} else {
reject(error);
}
};
/**
* Aborts all tasks.
*/
const abort = (): void => {
if (aborted) {
return;
}
aborted = true;
if (promises.length === 0) {
done();
} else {
for (const p of promises) {
p.abort?.();
}
Promise.all(promises).then(done, reject);
}
};
/**
* Runs a next task.
*/
const next = (): void => {
if (aborted) {
return;
}
if (queue.length === 0) {
if (promises.length === 0) {
done();
}
return;
}
const originalOutputStream = options.stdout;
const optionsClone = { ...options };
const writer = new MemoryStream();
if (options.aggregateOutput) {
optionsClone.stdout = writer as any;
}
const task = queue.shift()!;
// Note: This requires the actual runTask implementation from npm-run-all
// For now, this is a placeholder that would need to be implemented
const promise = Promise.resolve({
name: task.name,
code: 0,
signal: null,
}) as TaskPromise;
promises.push(promise);
promise.then(
(result) => {
remove(promises, promise);
if (aborted) {
return;
}
if (options.aggregateOutput) {
originalOutputStream.write(writer.toString());
}
// Check if the task failed as a result of a signal, and
// amend the exit code as a result.
if (
result.code === null &&
result.signal !== null &&
result.signal !== undefined
) {
// An exit caused by a signal must return a status code
// of 128 plus the value of the signal code.
// Ref: https://nodejs.org/api/process.html#process_exit_codes
result.code = 128 + convert(result.signal);
}
// Save the result.
results[task.index].code = result.code;
// Aborts all tasks if it's an error.
if (result.code) {
error = new Error(
`Task ${result.name} failed with code ${result.code}`
);
if (!options.continueOnError) {
abort();
return;
}
}
// Aborts all tasks if options.race is true.
if (options.race && !result.code) {
abort();
return;
}
// Call the next task.
next();
},
(thisError: Error) => {
remove(promises, promise);
if (!options.continueOnError || options.race) {
error = thisError;
abort();
return;
}
next();
}
);
};
const max = options.maxParallel;
const end =
typeof max === 'number' && max > 0
? Math.min(tasks.length, max)
: tasks.length;
for (let i = 0; i < end; ++i) {
next();
}
});
};