mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
73b90e958b
refactor: prefer templates rather than `+`
190 lines
5.3 KiB
TypeScript
190 lines
5.3 KiB
TypeScript
import { styleText } from 'node:util';
|
|
import { filterTestOutput } from './test-filter.ts';
|
|
import type {
|
|
CheckResultWithCoverage,
|
|
CoverageInfo,
|
|
ExecutionProgress,
|
|
ProcessManager,
|
|
} from './types.ts';
|
|
|
|
export const TUI = {
|
|
NAME_WIDTH: 36,
|
|
LINE_WIDTH: 50,
|
|
SPINNER_MS: 80,
|
|
} as const;
|
|
|
|
export const ANSI = {
|
|
moveUp: (n: number): string => `\x1b[${n}A`,
|
|
HIDE_CURSOR: '\x1b[?25l',
|
|
SHOW_CURSOR: '\x1b[?25h',
|
|
} as const;
|
|
|
|
const ICONS = {
|
|
WAITING: '⏳',
|
|
SPINNER: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const,
|
|
} as const;
|
|
|
|
export const isTTY = process.stdout.isTTY ?? false;
|
|
|
|
export function formatCompletedLine(
|
|
name: string,
|
|
success: boolean,
|
|
duration: number,
|
|
): string {
|
|
const nameCol = `${name} `.padEnd(TUI.NAME_WIDTH, '.');
|
|
const status = success ? styleText('green', '✓') : styleText('red', '✗');
|
|
const secs = String(Math.round(duration)).padStart(3);
|
|
// Visible: " " + name(36) + " " + icon + " " + secs(3) + "s" = 45
|
|
return ` ${nameCol} ${status} ${secs}s${' '.repeat(TUI.LINE_WIDTH - 45)}`;
|
|
}
|
|
|
|
export function formatWaitingLine(name: string): string {
|
|
const nameCol = `${name} `.padEnd(TUI.NAME_WIDTH, '.');
|
|
// Visible: " " + name(36) + " " + icon = 40
|
|
return ` ${nameCol} ${ICONS.WAITING}${' '.repeat(TUI.LINE_WIDTH - 40)}`;
|
|
}
|
|
|
|
function formatRunningLine(name: string, spinner: string): string {
|
|
const nameCol = `${name} `.padEnd(TUI.NAME_WIDTH, '.');
|
|
// Visible: " " + name(36) + " " + spinner = 40
|
|
return ` ${nameCol} ${spinner}${' '.repeat(TUI.LINE_WIDTH - 40)}`;
|
|
}
|
|
|
|
function formatInterruptedLine(name: string): string {
|
|
const nameCol = `${name} `.padEnd(TUI.NAME_WIDTH, '.');
|
|
// Visible: " " + name(36) + " " + "-" = 40
|
|
return ` ${nameCol} -${' '.repeat(TUI.LINE_WIDTH - 40)}`;
|
|
}
|
|
|
|
export function formatDuration(seconds: number): string {
|
|
return `${Math.round(seconds)}s`;
|
|
}
|
|
|
|
export function renderProgress(progress: ExecutionProgress): void {
|
|
if (!isTTY) {
|
|
return;
|
|
}
|
|
|
|
const spinner = ICONS.SPINNER[progress.spinnerFrame];
|
|
const elapsed = Math.round((Date.now() - progress.startTime) / 1000);
|
|
|
|
const lines: string[] = [];
|
|
for (let i = 0; i < progress.checks.length; i++) {
|
|
const entry = progress.results[i];
|
|
if (entry) {
|
|
lines.push(
|
|
formatCompletedLine(
|
|
entry.result.name,
|
|
entry.result.success,
|
|
entry.result.duration,
|
|
),
|
|
);
|
|
} else if (progress.startedIndices.has(i)) {
|
|
lines.push(formatRunningLine(progress.checks[i].name, spinner));
|
|
} else {
|
|
lines.push(formatWaitingLine(progress.checks[i].name));
|
|
}
|
|
}
|
|
lines.push(' '.repeat(TUI.LINE_WIDTH));
|
|
lines.push(`Total: ${elapsed}s`.padEnd(TUI.LINE_WIDTH));
|
|
|
|
const frame = `${ANSI.moveUp(progress.displayLines)}\r${lines.join('\n')}\n`;
|
|
process.stdout.write(frame);
|
|
}
|
|
|
|
export function renderFinalState(
|
|
progress: ExecutionProgress,
|
|
interrupted: boolean,
|
|
): void {
|
|
if (!isTTY) {
|
|
return;
|
|
}
|
|
|
|
const lines: string[] = [];
|
|
for (let i = 0; i < progress.checks.length; i++) {
|
|
const entry = progress.results[i];
|
|
if (entry) {
|
|
lines.push(
|
|
formatCompletedLine(
|
|
entry.result.name,
|
|
entry.result.success,
|
|
entry.result.duration,
|
|
),
|
|
);
|
|
} else {
|
|
lines.push(formatInterruptedLine(progress.checks[i].name));
|
|
}
|
|
}
|
|
|
|
while (lines.length < progress.displayLines) {
|
|
lines.push(' '.repeat(TUI.LINE_WIDTH));
|
|
}
|
|
|
|
const frame = `${ANSI.moveUp(progress.displayLines)}\r${lines.join('\n')}\n`;
|
|
process.stdout.write(frame);
|
|
|
|
if (interrupted) {
|
|
printFailedOutputs(progress.results);
|
|
const totalDuration = (Date.now() - progress.startTime) / 1000;
|
|
console.log('');
|
|
console.log(`Total: ${formatDuration(totalDuration)} (interrupted)`);
|
|
}
|
|
}
|
|
|
|
export function startRenderLoop(
|
|
progress: ExecutionProgress,
|
|
processManager: ProcessManager,
|
|
): void {
|
|
if (!isTTY) {
|
|
return;
|
|
}
|
|
processManager.renderInterval = setInterval(() => {
|
|
progress.spinnerFrame = (progress.spinnerFrame + 1) % ICONS.SPINNER.length;
|
|
renderProgress(progress);
|
|
}, TUI.SPINNER_MS);
|
|
}
|
|
|
|
export function stopRenderLoop(processManager: ProcessManager): void {
|
|
if (processManager.renderInterval) {
|
|
clearInterval(processManager.renderInterval);
|
|
processManager.renderInterval = null;
|
|
}
|
|
}
|
|
|
|
export function printFailedOutputs(
|
|
results: (CheckResultWithCoverage | null)[],
|
|
): void {
|
|
for (const entry of results) {
|
|
if (!entry || entry.result.success || !entry.result.output.trim()) {
|
|
continue;
|
|
}
|
|
let filteredOutput = entry.result.output;
|
|
if (entry.result.name.startsWith('test')) {
|
|
const filtered = filterTestOutput(entry.result.output);
|
|
if (filtered) {
|
|
filteredOutput = filtered;
|
|
}
|
|
}
|
|
console.log('');
|
|
console.log(`--- ${entry.result.name} output ---`);
|
|
console.log(filteredOutput);
|
|
console.log(`--- end ${entry.result.name} ---`);
|
|
}
|
|
}
|
|
|
|
export function printCoverageReport(coverage: CoverageInfo[]): void {
|
|
if (coverage.length === 0) {
|
|
return;
|
|
}
|
|
|
|
console.log('');
|
|
console.log('Not covered:');
|
|
|
|
const pathWidth = Math.max(...coverage.map((c) => c.file.length)) + 2;
|
|
|
|
for (const info of coverage) {
|
|
const lineRefs = info.uncoveredLines.map((l) => `L${l}`).join(', ');
|
|
console.log(` ${info.file.padEnd(pathWidth)}${lineRefs}`);
|
|
}
|
|
}
|