Skip to content

Commit

Permalink
fix(ng-dev): improve spinner experience when executed in a CI environ…
Browse files Browse the repository at this point in the history
…ment (#2426)

Improve the spinner experience in a CI environment so that it does not show the same message repeatedly, but also continues to show output to
prevent CI environments from treating the execution as inactive.

PR Close #2426
  • Loading branch information
josephperrott committed Nov 8, 2024
1 parent d99222b commit 11967c8
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 32 deletions.
5 changes: 2 additions & 3 deletions ng-dev/perf/workflow/workflow.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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 {
Expand Down
115 changes: 86 additions & 29 deletions ng-dev/utils/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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;
}
}

0 comments on commit 11967c8

Please sign in to comment.