UNPKG

nx

Version:

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

237 lines (236 loc) • 12.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createRunOneDynamicOutputRenderer = createRunOneDynamicOutputRenderer; 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 pc = require("picocolors"); 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 createRunOneDynamicOutputRenderer({ initiatingProject, 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 (renderDependentTargetsIntervalId) { clearInterval(renderDependentTargetsIntervalId); } } process.on('exit', () => clearRenderInterval()); process.on('SIGINT', () => clearRenderInterval()); process.on('SIGTERM', () => clearRenderInterval()); process.on('SIGHUP', () => clearRenderInterval()); const lifeCycle = {}; const start = process.hrtime(); const figures = await Promise.resolve().then(() => require('figures')); let state = 'EXECUTING_DEPENDENT_TARGETS'; const tasksToTerminalOutputs = {}; const totalTasks = tasks.length; const totalDependentTasks = totalTasks - 1; const totalTasksFromInitiatingProject = tasks.filter((t) => t.target.project === initiatingProject).length; // Tasks from the initiating project are treated differently, they forward their output const totalDependentTasksNotFromInitiatingProject = totalTasks - totalTasksFromInitiatingProject; const targetName = tasks[0].target.target; let dependentTargetsNumLines = 0; let totalCompletedTasks = 0; let totalSuccessfulTasks = 0; let totalFailedTasks = 0; let totalCachedTasks = 0; // Used to control the rendering of the spinner let dependentTargetsCurrentFrame = 0; let renderDependentTargetsIntervalId; const moveCursorToStartOfDependentTargetLines = () => { readline.cursorTo(process.stdout, 0); readline.moveCursor(process.stdout, 0, -dependentTargetsNumLines); }; const renderLines = (lines, dividerColor = 'cyan', renderDivider = true) => { let additionalLines = 0; if (renderDivider) { const dividerLines = output_1.output.getVerticalSeparatorLines(dividerColor); for (const line of dividerLines) { output_1.output.overwriteLine(line); } additionalLines += dividerLines.length; lines.push(''); } for (const line of lines) { output_1.output.overwriteLine(line); } dependentTargetsNumLines = lines.length + additionalLines; // clear any possible text below the cursor's position readline.clearScreenDown(process.stdout); }; const renderDependentTargets = (renderDivider = true) => { if (totalDependentTasksNotFromInitiatingProject <= 0) { return; } const max = cli_spinners_1.dots.frames.length - 1; const curr = dependentTargetsCurrentFrame; dependentTargetsCurrentFrame = curr >= max ? 0 : curr + 1; const linesToRender = ['']; const remainingDependentTasksNotFromInitiatingProject = totalDependentTasksNotFromInitiatingProject - totalCompletedTasks; switch (state) { case 'EXECUTING_DEPENDENT_TARGETS': if (totalFailedTasks === 0) { linesToRender.push(`${LEFT_PAD}${output_1.output.colors.cyan(cli_spinners_1.dots.frames[dependentTargetsCurrentFrame])}${SPACER}${output_1.output.dim(`Nx is waiting on ${remainingDependentTasksNotFromInitiatingProject} dependent project tasks before running tasks from`)} ${initiatingProject}${output_1.output.dim('...')}`); if (totalSuccessfulTasks > 0) { linesToRender.push(''); } } break; } if (totalFailedTasks > 0) { linesToRender.push(pc.dim(pc.red(`${LEFT_PAD}${output_1.output.colors.red(figures.cross)}${SPACER}${totalFailedTasks}${`/${totalCompletedTasks}`} dependent project tasks failed (see below)`))); } if (totalSuccessfulTasks > 0) { linesToRender.push(output_1.output.dim(`${LEFT_PAD}${output_1.output.dim(figures.tick)}${SPACER}${totalSuccessfulTasks}${`/${totalCompletedTasks}`} dependent project tasks succeeded ${output_1.output.dim(`[${totalCachedTasks} read from cache]`)}`)); } moveCursorToStartOfDependentTargetLines(); if (linesToRender.length > 1) { renderLines(linesToRender, 'gray', renderDivider && state !== 'EXECUTING_DEPENDENT_TARGETS'); } else { renderLines([]); } }; lifeCycle.startCommand = () => { renderDependentTargets(); }; lifeCycle.startTasks = (tasks) => { for (const task of tasks) { // Move from the dependent project tasks phase to the initiating project's targets if (task.target.project === initiatingProject && state !== 'EXECUTING_INITIATING_PROJECT_TARGET') { state = 'EXECUTING_INITIATING_PROJECT_TARGET'; clearRenderInterval(); renderDependentTargets(false); if (totalDependentTasksNotFromInitiatingProject > 0) { output_1.output.addNewline(); process.stdout.write(`${LEFT_PAD}${output_1.output.dim('Hint: you can run the command with')} --verbose ${output_1.output.dim('to see the full dependent project outputs')}` + os_1.EOL); output_1.output.addVerticalSeparator('gray'); } } } if (!renderDependentTargetsIntervalId && state === 'EXECUTING_DEPENDENT_TARGETS') { renderDependentTargetsIntervalId = setInterval(renderDependentTargets, 100); } }; lifeCycle.printTaskTerminalOutput = (task, cacheStatus, terminalOutput) => { if (task.target.project === initiatingProject) { output_1.output.logCommandOutput(task.id, cacheStatus, terminalOutput); } else { tasksToTerminalOutputs[task.id] = terminalOutput; } }; lifeCycle.endTasks = (taskResults) => { for (let t of taskResults) { totalCompletedTasks++; switch (t.status) { case 'remote-cache': case 'local-cache': case 'local-cache-kept-existing': totalCachedTasks++; totalSuccessfulTasks++; break; case 'success': totalSuccessfulTasks++; break; case 'failure': totalFailedTasks++; /** * A dependent project has failed so we stop executing and update the relevant * dependent project task messaging */ if (t.task.target.project !== initiatingProject) { clearRenderInterval(); renderDependentTargets(false); output_1.output.addVerticalSeparator('red'); output_1.output.logCommandOutput(t.task.id, t.status, tasksToTerminalOutputs[t.task.id]); } break; } delete tasksToTerminalOutputs[t.task.id]; } }; lifeCycle.endCommand = () => { clearRenderInterval(); const timeTakenText = (0, pretty_time_1.prettyTime)(process.hrtime(start)); if (totalSuccessfulTasks === totalTasks) { state = 'COMPLETED_SUCCESSFULLY'; const text = `Successfully ran ${(0, formatting_utils_1.formatTargetsAndProjects)([initiatingProject], [targetName], tasks)}`; const taskOverridesLines = []; 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) { taskOverridesLines.push(''); taskOverridesLines.push(`${EXTENDED_LEFT_PAD}${pc.dim(pc.green('With additional flags:'))}`); filteredOverrides .map(([flag, value]) => pc.dim(pc.green((0, formatting_utils_1.formatFlags)(EXTENDED_LEFT_PAD, flag, value)))) .forEach((arg) => taskOverridesLines.push(arg)); } const pinnedFooterLines = [ output_1.output.applyNxPrefix('green', output_1.output.colors.green(text) + output_1.output.dim(` (${timeTakenText})`)), ...taskOverridesLines, ]; 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.`)); } renderLines(pinnedFooterLines, 'green'); } else { state = 'COMPLETED_WITH_ERRORS'; let text = `Ran target ${output_1.output.bold(targetName)} for project ${output_1.output.bold(initiatingProject)}`; if (totalDependentTasks > 0) { text += ` and ${output_1.output.bold(totalDependentTasks)} task(s) they depend on`; } const taskOverridesLines = []; 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) { taskOverridesLines.push(''); taskOverridesLines.push(`${EXTENDED_LEFT_PAD}${pc.dim(pc.red('With additional flags:'))}`); filteredOverrides .map(([flag, value]) => pc.dim(pc.red((0, formatting_utils_1.formatFlags)(EXTENDED_LEFT_PAD, flag, value)))) .forEach((arg) => taskOverridesLines.push(arg)); } const viewLogs = (0, view_logs_utils_1.viewLogsFooterRows)(totalFailedTasks); renderLines([ output_1.output.applyNxPrefix('red', output_1.output.colors.red(text) + output_1.output.dim(` (${timeTakenText})`)), ...taskOverridesLines, '', `${LEFT_PAD}${output_1.output.colors.red(figures.cross)}${SPACER}${totalFailedTasks}${`/${totalCompletedTasks}`} failed`, `${LEFT_PAD}${output_1.output.dim(figures.tick)}${SPACER}${totalSuccessfulTasks}${`/${totalCompletedTasks}`} succeeded ${output_1.output.dim(`[${totalCachedTasks} read from cache]`)}`, ...viewLogs, ], 'red'); } resolveRenderIsDonePromise(); }; return { lifeCycle, renderIsDone }; }