UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

363 lines (362 loc) • 17.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createRunManyDynamicOutputRenderer = createRunManyDynamicOutputRenderer; const cliCursor = require("cli-cursor"); const cli_spinners_1 = require("cli-spinners"); const os_1 = require("os"); const readline = require("readline"); const output_1 = require("../../utils/output"); const pretty_time_1 = require("./pretty-time"); const formatting_utils_1 = require("./formatting-utils"); const view_logs_utils_1 = require("./view-logs-utils"); const LEFT_PAD = ` `; const SPACER = ` `; const EXTENDED_LEFT_PAD = ` `; /** * The following function is responsible for creating a life cycle with dynamic * outputs, meaning previous outputs can be rewritten or modified as new outputs * are added. It is therefore intended for use on a user's local machines. * * In CI environments the static equivalent of this life cycle should be used. * * NOTE: output.dim() should be preferred over output.colors.gray() because it * is much more consistently readable across different terminal color themes. */ async function createRunManyDynamicOutputRenderer({ projectNames, tasks, args, overrides, }) { cliCursor.hide(); // Show the cursor again after the process exits process.on('exit', () => { cliCursor.show(); }); let resolveRenderIsDonePromise; const renderIsDone = new Promise((resolve) => (resolveRenderIsDonePromise = resolve)).then(() => { clearRenderInterval(); cliCursor.show(); }); function clearRenderInterval() { if (renderIntervalId) { clearInterval(renderIntervalId); } } process.on('exit', () => clearRenderInterval()); process.on('SIGINT', () => clearRenderInterval()); process.on('SIGTERM', () => clearRenderInterval()); process.on('SIGHUP', () => clearRenderInterval()); const lifeCycle = {}; const isVerbose = overrides.verbose === true; const start = process.hrtime(); const figures = await Promise.resolve().then(() => require('figures')); const targets = args.targets; const totalTasks = tasks.length; const taskRows = tasks.map((task) => { return { task, status: 'pending', }; }); const failedTasks = new Set(); const tasksToTerminalOutputs = {}; const tasksToProcessStartTimes = {}; let hasTaskOutput = false; let pinnedFooterNumLines = 0; let totalCompletedTasks = 0; let totalSuccessfulTasks = 0; let totalFailedTasks = 0; let totalCachedTasks = 0; // Used to control the rendering of the spinner on each project row let currentFrame = 0; let renderIntervalId; const moveCursorToStartOfPinnedFooter = () => { readline.moveCursor(process.stdout, 0, -pinnedFooterNumLines); }; const renderPinnedFooter = (lines, dividerColor = 'cyan') => { let additionalLines = 0; if (hasTaskOutput) { const dividerLines = output_1.output.getVerticalSeparatorLines(dividerColor); for (const line of dividerLines) { output_1.output.overwriteLine(line); } additionalLines += dividerLines.length; } // Create vertical breathing room for cursor position under the pinned footer lines.push(''); for (const line of lines) { output_1.output.overwriteLine(line); } pinnedFooterNumLines = lines.length + additionalLines; // clear any possible text below the cursor's position readline.clearScreenDown(process.stdout); }; const printTaskResult = (task, status) => { moveCursorToStartOfPinnedFooter(); // If this is the very first output, add some vertical breathing room if (!hasTaskOutput) { output_1.output.addNewline(); } hasTaskOutput = true; switch (status) { case 'local-cache': writeCompletedTaskResultLine(`${output_1.output.colors.green(figures.tick) + SPACER + output_1.output.formatCommand(task.id)} ${output_1.output.dim('[local cache]')}`); if (isVerbose) { writeCommandOutputBlock(tasksToTerminalOutputs[task.id]); } break; case 'local-cache-kept-existing': writeCompletedTaskResultLine(`${output_1.output.colors.green(figures.tick) + SPACER + output_1.output.formatCommand(task.id)} ${output_1.output.dim('[existing outputs match the cache, left as is]')}`); if (isVerbose) { writeCommandOutputBlock(tasksToTerminalOutputs[task.id]); } break; case 'remote-cache': writeCompletedTaskResultLine(`${output_1.output.colors.green(figures.tick) + SPACER + output_1.output.formatCommand(task.id)} ${output_1.output.dim('[remote cache]')}`); if (isVerbose) { writeCommandOutputBlock(tasksToTerminalOutputs[task.id]); } break; case 'success': { const timeTakenText = (0, pretty_time_1.prettyTime)(process.hrtime(tasksToProcessStartTimes[task.id])); writeCompletedTaskResultLine(output_1.output.colors.green(figures.tick) + SPACER + output_1.output.formatCommand(task.id) + output_1.output.dim(` (${timeTakenText})`)); if (isVerbose) { writeCommandOutputBlock(tasksToTerminalOutputs[task.id]); } break; } case 'failure': output_1.output.addNewline(); output_1.output.addNewline(); writeCompletedTaskResultLine(output_1.output.colors.red(figures.cross) + SPACER + output_1.output.formatCommand(output_1.output.colors.red(task.id))); writeCommandOutputBlock(tasksToTerminalOutputs[task.id]); break; } delete tasksToTerminalOutputs[task.id]; renderPinnedFooter([]); renderRows(); }; const renderRows = () => { const max = cli_spinners_1.dots.frames.length - 1; const curr = currentFrame; currentFrame = curr >= max ? 0 : curr + 1; const additionalFooterRows = ['']; const runningTasks = taskRows.filter((row) => row.status === 'running'); const remainingTasks = totalTasks - totalCompletedTasks; if (runningTasks.length > 0) { additionalFooterRows.push(output_1.output.dim(`${LEFT_PAD}${output_1.output.colors.cyan(figures.arrowRight)}${SPACER}Executing ${runningTasks.length}/${remainingTasks} remaining tasks${runningTasks.length > 1 ? ' in parallel' : ''}...`)); additionalFooterRows.push(''); for (const runningTask of runningTasks) { additionalFooterRows.push(`${LEFT_PAD}${output_1.output.dim.cyan(cli_spinners_1.dots.frames[currentFrame])}${SPACER}${output_1.output.formatCommand(runningTask.task.id)}`); } /** * Reduce layout thrashing by ensuring that there is a relatively consistent * height for the area in which the task rows are rendered. * * We can look at the parallel flag to know how many rows are likely to be * needed in the common case and always render that at least that many. */ if (totalCompletedTasks !== totalTasks && Number.isInteger(args.parallel) && runningTasks.length < args.parallel) { // Don't bother with this optimization if there are fewer tasks remaining than rows required if (remainingTasks >= args.parallel) { for (let i = runningTasks.length; i < args.parallel; i++) { additionalFooterRows.push(''); } } } } if (totalSuccessfulTasks > 0 || totalFailedTasks > 0) { additionalFooterRows.push(''); } if (totalSuccessfulTasks > 0) { additionalFooterRows.push(`${LEFT_PAD}${output_1.output.colors.green(figures.tick)}${SPACER}${totalSuccessfulTasks}${`/${totalCompletedTasks}`} succeeded ${output_1.output.dim(`[${totalCachedTasks} read from cache]`)}`); } if (totalFailedTasks > 0) { additionalFooterRows.push(`${LEFT_PAD}${output_1.output.colors.red(figures.cross)}${SPACER}${totalFailedTasks}${`/${totalCompletedTasks}`} failed`); } moveCursorToStartOfPinnedFooter(); if (additionalFooterRows.length > 1) { const text = `Running ${(0, formatting_utils_1.formatTargetsAndProjects)(projectNames, targets, tasks)}`; const taskOverridesRows = []; const filteredOverrides = Object.entries(overrides).filter( // Don't print the data passed through from the version subcommand to the publish executor options, it could be quite large and it's an implementation detail. ([flag]) => flag !== 'nxReleaseVersionData'); if (filteredOverrides.length > 0) { taskOverridesRows.push(''); taskOverridesRows.push(`${EXTENDED_LEFT_PAD}${output_1.output.dim.cyan('With additional flags:')}`); filteredOverrides .map(([flag, value]) => output_1.output.dim.cyan((0, formatting_utils_1.formatFlags)(EXTENDED_LEFT_PAD, flag, value))) .forEach((arg) => taskOverridesRows.push(arg)); } const pinnedFooterLines = [ output_1.output.applyNxPrefix('cyan', output_1.output.colors.cyan(text)), ...taskOverridesRows, ...additionalFooterRows, ]; // Vertical breathing room when there isn't yet any output or divider if (!hasTaskOutput) { pinnedFooterLines.unshift(''); } renderPinnedFooter(pinnedFooterLines); } else { renderPinnedFooter([]); } }; lifeCycle.startCommand = () => { if (projectNames.length <= 0) { renderPinnedFooter([ '', output_1.output.applyNxPrefix('gray', `No projects with ${(0, formatting_utils_1.formatTargetsAndProjects)(projectNames, targets, tasks)} were run`), ]); resolveRenderIsDonePromise(); return; } renderPinnedFooter([]); }; lifeCycle.endCommand = () => { clearRenderInterval(); const timeTakenText = (0, pretty_time_1.prettyTime)(process.hrtime(start)); moveCursorToStartOfPinnedFooter(); if (totalTasks === 0) { renderPinnedFooter([output_1.output.applyNxPrefix('gray', 'No tasks were run')]); resolveRenderIsDonePromise(); return; } if (totalSuccessfulTasks === totalTasks) { const text = `Successfully ran ${(0, formatting_utils_1.formatTargetsAndProjects)(projectNames, targets, tasks)}`; const taskOverridesRows = []; const filteredOverrides = Object.entries(overrides).filter( // Don't print the data passed through from the version subcommand to the publish executor options, it could be quite large and it's an implementation detail. ([flag]) => flag !== 'nxReleaseVersionData'); if (filteredOverrides.length > 0) { taskOverridesRows.push(''); taskOverridesRows.push(`${EXTENDED_LEFT_PAD}${output_1.output.dim.green('With additional flags:')}`); filteredOverrides .map(([flag, value]) => output_1.output.dim.green((0, formatting_utils_1.formatFlags)(EXTENDED_LEFT_PAD, flag, value))) .forEach((arg) => taskOverridesRows.push(arg)); } const pinnedFooterLines = [ output_1.output.applyNxPrefix('green', output_1.output.colors.green(text) + output_1.output.dim.white(` (${timeTakenText})`)), ...taskOverridesRows, ]; if (totalCachedTasks > 0) { pinnedFooterLines.push(output_1.output.dim(`${os_1.EOL}Nx read the output from the cache instead of running the command for ${totalCachedTasks} out of ${totalTasks} tasks.`)); } renderPinnedFooter(pinnedFooterLines, 'green'); } else { const text = `Ran ${(0, formatting_utils_1.formatTargetsAndProjects)(projectNames, targets, tasks)}`; const taskOverridesRows = []; const filteredOverrides = Object.entries(overrides).filter( // Don't print the data passed through from the version subcommand to the publish executor options, it could be quite large and it's an implementation detail. ([flag]) => flag !== 'nxReleaseVersionData'); if (filteredOverrides.length > 0) { taskOverridesRows.push(''); taskOverridesRows.push(`${EXTENDED_LEFT_PAD}${output_1.output.dim.red('With additional flags:')}`); filteredOverrides .map(([flag, value]) => output_1.output.dim.red((0, formatting_utils_1.formatFlags)(EXTENDED_LEFT_PAD, flag, value))) .forEach((arg) => taskOverridesRows.push(arg)); } const numFailedToPrint = 5; const failedTasksForPrinting = Array.from(failedTasks).slice(0, numFailedToPrint); const failureSummaryRows = [ output_1.output.applyNxPrefix('red', output_1.output.colors.red(text) + output_1.output.dim.white(` (${timeTakenText})`)), ...taskOverridesRows, '', output_1.output.dim(`${LEFT_PAD}${output_1.output.dim(figures.tick)}${SPACER}${totalSuccessfulTasks}${`/${totalCompletedTasks}`} succeeded ${output_1.output.dim(`[${totalCachedTasks} read from cache]`)}`), '', `${LEFT_PAD}${output_1.output.colors.red(figures.cross)}${SPACER}${totalFailedTasks}${`/${totalCompletedTasks}`} targets failed, including the following:`, '', `${failedTasksForPrinting .map((t) => `${EXTENDED_LEFT_PAD}${output_1.output.colors.red('-')} ${output_1.output.formatCommand(t.toString())}`) .join('\n')}`, ]; if (failedTasks.size > numFailedToPrint) { failureSummaryRows.push(output_1.output.dim(`${EXTENDED_LEFT_PAD}...and ${failedTasks.size - numFailedToPrint} more...`)); } failureSummaryRows.push(...(0, view_logs_utils_1.viewLogsFooterRows)(failedTasks.size)); renderPinnedFooter(failureSummaryRows, 'red'); } resolveRenderIsDonePromise(); }; lifeCycle.startTasks = (tasks) => { for (const task of tasks) { tasksToProcessStartTimes[task.id] = process.hrtime(); } for (const taskRow of taskRows) { if (tasks.indexOf(taskRow.task) > -1) { taskRow.status = 'running'; } } if (!renderIntervalId) { renderIntervalId = setInterval(renderRows, 100); } }; lifeCycle.printTaskTerminalOutput = (task, _cacheStatus, output) => { tasksToTerminalOutputs[task.id] = output; }; lifeCycle.endTasks = (taskResults) => { for (let t of taskResults) { totalCompletedTasks++; const matchingTaskRow = taskRows.find((r) => r.task.id === t.task.id); if (matchingTaskRow) { matchingTaskRow.status = t.status; } switch (t.status) { case 'remote-cache': case 'local-cache': case 'local-cache-kept-existing': totalCachedTasks++; totalSuccessfulTasks++; break; case 'success': totalSuccessfulTasks++; break; case 'failure': totalFailedTasks++; failedTasks.add(t.task.id); break; } printTaskResult(t.task, t.status); } }; return { lifeCycle, renderIsDone }; } function writeCompletedTaskResultLine(line) { output_1.output.overwriteLine(LEFT_PAD + line); } /** * There's not much we can do in order to "neaten up" the outputs of * commands we do not control, but at the very least we can trim any * leading whitespace and any _excess_ trailing newlines so that there * isn't unncecessary vertical whitespace. */ function writeCommandOutputBlock(commandOutput) { commandOutput = commandOutput || ''; commandOutput = commandOutput.trimStart(); const lines = commandOutput.split(/\r?\n/); let totalTrailingEmptyLines = 0; for (let i = lines.length - 1; i >= 0; i--) { if (lines[i] !== '') { break; } totalTrailingEmptyLines++; } if (totalTrailingEmptyLines > 1) { const linesToRemove = totalTrailingEmptyLines - 1; lines.splice(lines.length - linesToRemove, linesToRemove); } lines.push(''); // Indent the command output to make it look more "designed" in the context of the dynamic output lines.forEach((l) => output_1.output.overwriteLine(`${EXTENDED_LEFT_PAD}${l}`)); }