UNPKG

@stackbit/utils

Version:
123 lines (115 loc) 4.36 kB
import _ from 'lodash'; import childProcess, { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'child_process'; import stripAnsi from 'strip-ansi'; import { Logger } from './logger-types'; export type LogTarget = 'stdout' | 'stderr' | 'error'; export interface LogProcessOptions { onLog?: { callback: (name: string, target: LogTarget, line: string) => void; pattern: RegExp; target: LogTarget; }[]; info?: Record<string, any>; } interface LogBufferHandler { handler: (target: LogTarget, data: string) => void; flush: () => void; } export function createLogBufferHandler(name: string, logger: Logger, { onLog, info = {} }: LogProcessOptions = {}): LogBufferHandler { const lineBuffer: Record<LogTarget, string> = { stdout: '', stderr: '', error: '' }; function logLine(target: LogTarget, line: string) { if (_.isEmpty(_.trim(stripAnsi(line)))) { return; } if (target === 'stdout') { logger.debug(`[${name}]: ${line}`, info); } else { // log.error - too much errors logs generated which are not really errors logger.debug(`[${name}]: ${line}`, info); } _.forEach(onLog, (onLogItem) => { if (_.has(onLogItem, 'callback')) { const pattern = _.get(onLogItem, 'pattern'); if (pattern && !pattern.test(stripAnsi(line))) { return; } const filterTarget = _.get(onLogItem, 'target', null); if (filterTarget && filterTarget !== target) { return; } onLogItem.callback(name, target, line); } }); } return { // If handler gets unterminated line, save it until we get line ending // For example assume data stream has two lines called in following order: // handler("hello world\nhow are ") // handler("you?\n") // First time the handler called, we can output the first line: "hello world". // The second line was not terminated so we need to wait until we receive its ending // then we can output the full second line: "how are you?" handler: (target: LogTarget, data: string) => { const lines = _.split(data, '\n'); if (lineBuffer[target]) { lines[0] = lineBuffer[target] + lines[0]; lineBuffer[target] = ''; } if (_.last(lines) !== '') { lineBuffer[target] = lines.pop()!; } _.forEach(lines, (line) => { logLine(target, line); }); }, flush: () => { _.forEach(lineBuffer, (line, target) => { const logTarget = target as LogTarget; logLine(logTarget, line); lineBuffer[logTarget] = ''; }); } }; } export function logProcess(p: ChildProcessWithoutNullStreams, name: string, logger: Logger, options?: LogProcessOptions) { const bufferHandler = createLogBufferHandler(name, logger, options); p.stdout.on('data', _.partial(bufferHandler.handler, 'stdout')); p.stderr.on('data', _.partial(bufferHandler.handler, 'stderr')); p.on('error', (error) => { logger.debug(`[${name}] error: ${error.message}`); }); p.on('close', (code) => { bufferHandler.flush(); if (code != 0) { logger.debug(`[${name}] closed with code: ${code}`); } }); p.on('exit', (code) => { bufferHandler.flush(); if (code != 0) { logger.debug(`[${name}] exited with code: ${code}`); } }); } export function getProcessPromise(p: ChildProcessWithoutNullStreams): Promise<string> { return new Promise((resolve, reject) => { let data = ''; let stderr = ''; p.stdout.on('data', (out) => (data += out)); p.stderr.on('data', (out) => (stderr += out)); p.on('close', (code) => { if (code !== 0) { reject(new Error(`process exited with code: ${code}, stderr: ${stderr}`)); } else { resolve(data); } }); p.on('error', (err) => { reject(err); }); }); }