UNPKG

turbowatch

Version:

Extremely fast file change detector and task orchestrator for Node.js.

185 lines (146 loc) 4.41 kB
// cspell:words nothrow import { AbortError, UnexpectedError } from './errors'; import { findNearestDirectory } from './findNearestDirectory'; import { killPsTree } from './killPsTree'; import { Logger } from './Logger'; import { type Throttle } from './types'; import chalk from 'chalk'; import randomColor from 'randomcolor'; import { throttle } from 'throttle-debounce'; import { $ } from 'zx'; const log = Logger.child({ namespace: 'createSpawn', }); const prefixLines = (subject: string, prefix: string): string => { const response: string[] = []; for (const fragment of subject.split('\n')) { response.push(prefix + fragment); } return response.join('\n'); }; export const createSpawn = ( taskId: string, { cwd = process.cwd(), abortSignal, outputPrefix = true, throttleOutput, triggerName, triggerHexColor, }: { abortSignal?: AbortSignal; cwd?: string; outputPrefix?: boolean; throttleOutput?: Throttle; triggerHexColor?: string; triggerName?: string; } = {}, ) => { let stdoutBuffer: string[] = []; let stderrBuffer: string[] = []; const flush = () => { if (stdoutBuffer.length) { // eslint-disable-next-line no-console console.log(stdoutBuffer.join('\n')); } if (stderrBuffer.length) { // eslint-disable-next-line no-console console.error(stderrBuffer.join('\n')); } stdoutBuffer = []; stderrBuffer = []; }; const output = throttle( throttleOutput?.delay, () => { flush(); }, { noLeading: true, }, ); const colorText = chalk.hex( triggerHexColor || randomColor({ luminosity: 'dark' }), ); return async (pieces: TemplateStringsArray, ...args: any[]) => { const binPath = (await findNearestDirectory('node_modules', cwd)) + '/.bin'; $.cwd = cwd; $.prefix = `set -euo pipefail; export PATH="${binPath}:$PATH";`; let onStdout: (chunk: Buffer) => void; let onStderr: (chunk: Buffer) => void; const formatChunk = (chunk: Buffer) => { const content = chunk.toString().trimEnd(); if (!outputPrefix) { return content; } const prefixTriggerName = triggerName ? triggerName + ' ' : ''; return prefixLines( content, colorText(`${prefixTriggerName}${taskId}`) + ' > ', ); }; if (throttleOutput?.delay) { onStdout = (chunk: Buffer) => { stdoutBuffer.push(formatChunk(chunk)); output(); }; onStderr = (chunk: Buffer) => { stderrBuffer.push(formatChunk(chunk)); output(); }; } else { onStdout = (chunk: Buffer) => { // eslint-disable-next-line no-console console.log(formatChunk(chunk)); }; onStderr = (chunk: Buffer) => { // eslint-disable-next-line no-console console.error(formatChunk(chunk)); }; } if (abortSignal?.aborted) { throw new UnexpectedError( 'Attempted to spawn a process after the task was aborted.', ); } // eslint-disable-next-line promise/prefer-await-to-then const processPromise = $(pieces, ...args) .nothrow() .quiet(); processPromise.stdout.on('data', onStdout); processPromise.stderr.on('data', onStderr); if (abortSignal) { const kill = () => { const pid = processPromise.child?.pid; if (!pid) { log.warn('no process to kill'); return; } // TODO make this configurable // eslint-disable-next-line promise/prefer-await-to-then killPsTree(pid, 5_000).then(() => { log.debug('task %s was killed', taskId); processPromise.stdout.off('data', onStdout); processPromise.stderr.off('data', onStderr); }); }; abortSignal.addEventListener('abort', kill, { once: true, }); // eslint-disable-next-line promise/prefer-await-to-then processPromise.finally(() => { abortSignal.removeEventListener('abort', kill); }); } const result = await processPromise; flush(); if (result.exitCode === 0) { return result; } if (abortSignal?.aborted) { throw new AbortError('Program was aborted.'); } log.error('task %s exited with an error', taskId); throw new Error('Program exited with code ' + result.exitCode + '.'); }; };