UNPKG

@boost/core

Version:

Robust pipeline for creating dev tools that separate logic into routines and tasks.

400 lines (399 loc) 12.2 kB
"use strict"; /* eslint-disable no-console */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const exit_1 = __importDefault(require("exit")); const term_size_1 = __importDefault(require("term-size")); const ansi_escapes_1 = __importDefault(require("ansi-escapes")); const debug_1 = require("@boost/debug"); const event_1 = require("@boost/event"); const Emitter_1 = __importDefault(require("./Emitter")); const SignalError_1 = __importDefault(require("./SignalError")); // 8 FPS (60 FPS is actually too fast as it tears) exports.FPS_RATE = 125; // Bind our writable streams for easy access exports.BOUND_WRITERS = { stderr: process.stderr.write.bind(process.stderr), stdout: process.stdout.write.bind(process.stdout), }; exports.WRAPPED_STREAMS = { stderr: false, stdout: false, }; class Console extends Emitter_1.default { constructor(tool, /* test only */ testWriters = exports.BOUND_WRITERS) { super(); this.bufferedStreams = []; this.errorLogs = []; this.logs = []; this.outputQueue = []; this.renderTimer = null; this.state = { disabled: false, final: false, started: false, stopped: false, }; /** * Handle uncaught exceptions and unhandled rejections that bubble up. */ this.handleFailure = (error) => { this.start(); this.debug('Uncaught exception or unresolved promise handled'); this.stop(error); exit_1.default(2); }; /** * Handle SIGINT and SIGTERM interruptions. */ this.handleSignal = (signal) => { this.start(); this.debug('SIGINT or SIGTERM handled'); this.stop(new SignalError_1.default(this.tool.msg('errors:processTerminated'), signal)); exit_1.default(2); }; this.debug = debug_1.createDebugger('console'); this.tool = tool; this.writers = testWriters; this.onError = new event_1.Event('error'); this.onRoutine = new event_1.Event('routine'); this.onRoutines = new event_1.Event('routines'); this.onStart = new event_1.Event('start'); this.onStop = new event_1.Event('stop'); this.onTask = new event_1.Event('task'); this.onTasks = new event_1.Event('tasks'); // istanbul ignore next if (process.env.NODE_ENV !== 'test') { process .on('SIGINT', this.handleSignal) .on('SIGTERM', this.handleSignal) .on('uncaughtException', this.handleFailure) // @ts-ignore .on('unhandledRejection', this.handleFailure); } } /** * Disable the render loop entirely. */ disable() { this.state.disabled = true; return this; } /** * Display a footer after all output. */ displayFooter() { const { footer } = this.tool.options; if (footer) { this.out(footer, 1); } } /** * Display a header before all output. */ displayHeader() { const { header } = this.tool.options; if (header) { this.out(header, 1); } } /** * Enable the render loop. */ enable() { this.state.disabled = false; return this; } /** * Write a message to `stderr` with optional trailing newline(s). */ err(message, nl = 0) { if (!this.isSilent()) { this.writers.stderr(message + '\n'.repeat(nl)); } return this; } /** * Flush buffered stream output. */ flushBufferedStreams() { this.bufferedStreams.forEach(buffer => { buffer(); }); return this; } /** * Flush the top output block in the queue. */ flushOutputQueue() { const outputs = this.outputQueue.filter((out, i) => i === 0 || out.isConcurrent()); // Erase the previous output outputs.forEach(output => { output.erasePrevious(); }); // Write buffered output this.flushBufferedStreams(); // Write the next output outputs.forEach(output => { output.render(); }); // Remove completed outputs this.outputQueue = this.outputQueue.filter(out => !out.isComplete()); // Stop the render loop once the queue is empty if (this.isEmptyQueue()) { this.stopRenderLoop(); } else { this.startRenderLoop(); } return this; } /** * Hide the console cursor. */ hideCursor() { console.warn('Console#hideCursor is deprecated. Use Reporter#hideCursor instead.'); this.out(ansi_escapes_1.default.cursorHide); return this; } /** * Return true if the render loop has been disabled. */ isDisabled() { return this.state.disabled; } /** * Return true if the output queue is empty. */ isEmptyQueue() { return this.outputQueue.length === 0; } /** * Return true if the final render. */ isFinalRender() { return this.state.final; } /** * Return true if the there should be no output. */ isSilent() { // Check all objects since these might not have been loaded yet return this.tool && this.tool.config && this.tool.config.silent; } /** * Return true if the defined stream has been wrapped by the console layer. */ isStreamWrapped(type) { return !!exports.WRAPPED_STREAMS[type]; } /** * Log a message to display on success during the final render. */ log(message) { this.logs.push(message); return this; } /** * Log a live message to display during the rendering process. */ logLive(message) { console.warn('Console#logLive is deprecated. Use console.log instead.'); // Write to the wrapped buffer process.stdout.write(message); return this; } /** * Log an error message to display on failure during the final render. */ logError(message) { this.errorLogs.push(message); return this; } /** * Write a message to `stdout` with optional trailing newline(s). */ out(message, nl = 0) { if (!this.isSilent()) { this.writers.stdout(message + '\n'.repeat(nl)); } return this; } /** * Enqueue a block of output to be rendered. */ render(output) { if (this.isDisabled()) { throw new Error('Output cannot be enqueued as the render loop has been disabled. This is usually caused by conflicting reporters.'); } if (!this.outputQueue.includes(output)) { this.outputQueue.push(output); } // Only run the render loop when output is enqueued this.startRenderLoop(); return this; } /** * Handle the final rendering of all output before stopping. */ renderFinalOutput(error) { this.debug('Rendering final console output'); this.state.final = true; // Mark all output as final this.outputQueue.forEach(output => { output.enqueue(true); }); // Recursively render the remaining output while (!this.isEmptyQueue()) { this.flushOutputQueue(); } // Stop the render loop this.stopRenderLoop(); if (error) { if (this.errorLogs.length > 0) { this.err(`\n${this.errorLogs.join('\n')}\n`); } this.emit('error', [error]); this.onError.emit([error]); } else { if (this.logs.length > 0) { this.out(`\n${this.logs.join('\n')}\n`); } this.displayFooter(); } // Flush any stream output that still exists this.flushBufferedStreams(); // Show the cursor incase it has been hidden this.out(ansi_escapes_1.default.cursorShow); } /** * Reset the cursor back to the bottom of the console. */ resetCursor() { console.warn('Console#resetCursor is deprecated. Use Reporter#resetCursor instead.'); this.out(ansi_escapes_1.default.cursorTo(0, term_size_1.default().rows)); return this; } /** * Show the console cursor. */ showCursor() { console.warn('Console#showCursor is deprecated. Use Reporter#showCursor instead.'); this.out(ansi_escapes_1.default.cursorShow); return this; } /** * Start the console by wrapping streams and buffering output. */ start(args = []) { if (this.state.started) { return this; } this.debug('Starting console render loop'); this.emit('start', args); this.onStart.emit(args); this.wrapStreams(); this.displayHeader(); this.state.started = true; return this; } /** * Automatically render the console in a timeout loop at 8 FPS. */ startRenderLoop() { if (this.isSilent() || this.isDisabled() || this.renderTimer) { return; } this.renderTimer = setTimeout(() => { this.renderTimer = null; this.flushOutputQueue(); }, exports.FPS_RATE); } /** * Stop the console rendering process. */ stop(error = null) { if (this.state.stopped) { return; } if (error) { this.debug('Stopping console with an error'); this.errorLogs.push(...this.logs); this.logs = []; } else { this.debug('Stopping console render loop'); } this.renderFinalOutput(error); this.unwrapStreams(); this.emit('stop', [error]); this.onStop.emit([error]); this.state.stopped = true; this.state.started = false; } /** * Stop the background render loop. */ stopRenderLoop() { if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = null; } } /** * Unwrap the native console and reset it back to normal. */ unwrapStreams() { if (this.isSilent() || process.env.NODE_ENV === 'test') { return; } ['stderr', 'stdout'].forEach(key => { const name = key; if (!this.isStreamWrapped(name)) { return; } this.debug('Unwrapping `%s` stream', name); process[name].write = this.writers[name]; exports.WRAPPED_STREAMS[name] = false; }); } /** * Wrap the `stdout` and `stderr` streams and buffer the output as * to not collide with our render loop. */ wrapStreams() { if (this.isSilent() || this.isDisabled() || process.env.NODE_ENV === 'test') { return; } ['stderr', 'stdout'].forEach(key => { const name = key; const stream = process[name]; let buffer = ''; if (this.isStreamWrapped(name)) { return; } this.debug('Wrapping `%s` stream', name); this.bufferedStreams.push(() => { if (stream.isTTY && buffer) { this.writers[name](buffer); } buffer = ''; }); stream.write = (chunk) => { // No output, display immediately if (this.isEmptyQueue()) { this.writers[name](chunk); } else { buffer += String(chunk); } return true; }; exports.WRAPPED_STREAMS[name] = true; }); } } exports.default = Console;