diff --git a/ng-dev/perf/workflow/workflow.ts b/ng-dev/perf/workflow/workflow.ts index 0fe50576a..f4775a6cd 100644 --- a/ng-dev/perf/workflow/workflow.ts +++ b/ng-dev/perf/workflow/workflow.ts @@ -1,10 +1,9 @@ import {ChildProcess} from '../../utils/child-process.js'; -import {green} from '../../utils/logging.js'; import {Spinner} from '../../utils/spinner.js'; import {Workflow} from './loader.js'; export async function measureWorkflow({name, workflow, prepare, cleanup}: Workflow) { - const spinner = new Spinner(''); + const spinner = new Spinner(); try { if (prepare) { spinner.update('Preparing environment for workflow execution'); @@ -32,7 +31,7 @@ export async function measureWorkflow({name, workflow, prepare, cleanup}: Workfl const results = performance.measure(name, 'start', 'end'); - spinner.complete(` ${green('✓')} ${name}: ${results.duration.toFixed(2)}ms`); + spinner.success(`${name}: ${results.duration.toFixed(2)}ms`); return results.toJSON(); } finally { diff --git a/ng-dev/utils/spinner.ts b/ng-dev/utils/spinner.ts index af614f303..74fbf4902 100644 --- a/ng-dev/utils/spinner.ts +++ b/ng-dev/utils/spinner.ts @@ -7,27 +7,74 @@ */ import {cursorTo, clearLine} from 'readline'; +import {green, red} from './logging.js'; +/** Whether execution is in a CI environment. */ +const IS_CI = process.env['CI']; /** ANSI escape code to hide cursor in terminal. */ const hideCursor = '\x1b[?25l'; /** ANSI escape code to show cursor in terminal. */ const showCursor = '\x1b[?25h'; export class Spinner { - /** Whether the spinner is currently running. */ - private isRunning = true; + /** Whether the spinner is marked as completed. */ + private completed = false; /** The id of the interval being used to trigger frame printing. */ - private intervalId = setInterval(() => this.printFrame(), 125); + private intervalId = setInterval(() => this.printFrame(), IS_CI ? 2500 : 125); /** The characters to iterate through to create the appearance of spinning in the spinner. */ private spinnerCharacters = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; /** The index of the spinner character used in the frame. */ private currentSpinnerCharacterIndex = 0; /** The current text of the spinner. */ - private text: string = ''; + private _text: string = ''; + private set text(text: string | undefined) { + this._text = text || this._text; + this.printFrame(this.getNextSpinnerCharacter(), text); + } + private get text(): string { + return this._text; + } + + constructor(); + constructor(text: string); + constructor(text?: string) { + this.hideCursor(); + this.text = text; + } + + /** Updates the spinner text with the provided text. */ + update(text: string) { + this.text = text; + } - constructor(text: string) { - process.stdout.write(hideCursor); - this.update(text); + /** Completes the spinner marking it as successful with a `✓`. */ + success(text: string): void { + this._complete(green('✓'), text); + } + + /** Completes the spinner marking it as failing with an `✘`. */ + failure(text: string): void { + this._complete(red('✘'), text); + } + + /** Completes the spinner. */ + complete() { + this._complete('', this.text); + } + + /** + * Internal implementation for completing the spinner, marking it as completed, and printing the + * final frame. + */ + private _complete(prefix: string, text: string) { + if (this.completed) { + return; + } + clearInterval(this.intervalId); + this.printFrame(prefix, text); + process.stdout.write('\n'); + this.showCursor(); + this.completed = true; } /** Get the next spinner character. */ @@ -37,36 +84,46 @@ export class Spinner { return this.spinnerCharacters[this.currentSpinnerCharacterIndex]; } - /** Print the current text for the spinner to the */ - private printFrame(prefix = this.getNextSpinnerCharacter(), text = this.text) { + /** + * Print the next frame either in CI mode or local terminal mode based on whether the script is run in a + * CI environment. + */ + private printFrame(prefix = this.getNextSpinnerCharacter(), text?: string): void { + if (IS_CI) { + this.printNextCIFrame(text); + } else { + this.printNextLocalFrame(prefix, text); + } + } + + /** Print the current text for the spinner to the terminal. */ + private printNextLocalFrame(prefix: string, text?: string) { cursorTo(process.stdout, 0); - process.stdout.write(` ${prefix} ${text}`); + process.stdout.write(` ${prefix} ${text || this.text}`); // Clear to the right of the cursor location in case the new frame is shorter than the previous. clearLine(process.stdout, 1); - cursorTo(process.stdout, 0); } - /** Updates the spinner text with the provided text. */ - update(text: string) { - this.text = text; - this.printFrame(this.spinnerCharacters[this.currentSpinnerCharacterIndex]); + /** Print the next expected piece for the spinner to stdout for CI usage. */ + private printNextCIFrame(text?: string) { + if (text) { + process.stdout.write(`\n${text}.`); + return; + } + process.stdout.write('.'); } - /** Completes the spinner. */ - complete(): void; - complete(text: string): void; - complete(text?: string) { - if (!this.isRunning) { - return; + /** Hide the cursor in the terminal, only executed in local environments. */ + private hideCursor() { + if (!IS_CI) { + process.stdout.write(hideCursor); } - clearInterval(this.intervalId); - clearLine(process.stdout, 1); - cursorTo(process.stdout, 0); - if (text) { - process.stdout.write(text); - process.stdout.write('\n'); + } + + /** Resume showing the cursor in the terminal, only executed in local environments. */ + private showCursor() { + if (!IS_CI) { + process.stdout.write(showCursor); } - process.stdout.write(showCursor); - this.isRunning = false; } }