Skip to content

Commit f9f139d

Browse files
orochaanatemoo-re
andauthoredDec 14, 2024··
feat(@clack/prompts): adapt spinner to CI environment (#169)
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
1 parent a0e28ac commit f9f139d

File tree

6 files changed

+70
-10
lines changed

6 files changed

+70
-10
lines changed
 

‎.changeset/thin-moose-tease.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clack/prompts': patch
3+
---
4+
5+
Adapts `spinner` output for static CI environments

‎examples/basic/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
},
1111
"scripts": {
1212
"start": "jiti ./index.ts",
13-
"spinner": "jiti ./spinner.ts"
13+
"spinner": "jiti ./spinner.ts",
14+
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts"
1415
},
1516
"devDependencies": {
1617
"jiti": "^1.17.0"

‎examples/basic/spinner-ci.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* This example addresses a issue reported in GitHub Actions where `spinner` was excessively writing messages,
3+
* leading to confusion and cluttered output.
4+
* To enhance the CI workflow and provide a smoother experience,
5+
* the following changes have been made only for CI environment:
6+
* - Messages will now only be written when a `spinner` method is called and the message updated, preventing unnecessary message repetition.
7+
* - There will be no loading dots animation, instead it will be always `...`
8+
* - Instead of erase the previous message, action that is blocked during CI, it will just write a new one.
9+
*
10+
* Issue: https://github.com/natemoo-re/clack/issues/168
11+
*/
12+
import * as p from '@clack/prompts';
13+
14+
const s = p.spinner();
15+
let progress = 0;
16+
let counter = 0;
17+
let loop: NodeJS.Timer;
18+
19+
p.intro('Running spinner in CI environment');
20+
s.start('spinner.start');
21+
new Promise((resolve) => {
22+
loop = setInterval(() => {
23+
if (progress % 1000 === 0) {
24+
counter++;
25+
}
26+
progress += 100;
27+
s.message(`spinner.message [${counter}]`);
28+
if (counter > 6) {
29+
clearInterval(loop);
30+
resolve(true);
31+
}
32+
}, 100);
33+
}).then(() => {
34+
s.stop('spinner.stop');
35+
p.outro('Done');
36+
});

‎examples/basic/spinner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as p from '@clack/prompts';
33
p.intro('spinner start...');
44

55
const spin = p.spinner();
6-
const total = 10000;
6+
const total = 6000;
77
let progress = 0;
88
spin.start();
99

‎packages/prompts/src/index.ts

+25-8
Original file line numberDiff line numberDiff line change
@@ -643,11 +643,13 @@ export const log = {
643643
export const spinner = () => {
644644
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
645645
const delay = unicode ? 80 : 120;
646+
const isCI = process.env.CI === 'true';
646647

647648
let unblock: () => void;
648649
let loop: NodeJS.Timeout;
649650
let isSpinnerActive = false;
650651
let _message = '';
652+
let _prevMessage: string | undefined = undefined;
651653

652654
const handleExit = (code: number) => {
653655
const msg = code > 1 ? 'Something went wrong' : 'Canceled';
@@ -676,44 +678,59 @@ export const spinner = () => {
676678
process.removeListener('exit', handleExit);
677679
};
678680

681+
682+
const clearPrevMessage = () => {
683+
if (_prevMessage === undefined) return;
684+
if (isCI) process.stdout.write('\n');
685+
const prevLines = _prevMessage.split('\n');
686+
process.stdout.write(cursor.move(-999, prevLines.length - 1));
687+
process.stdout.write(erase.down(prevLines.length));
688+
};
689+
690+
const parseMessage = (msg: string): string => {
691+
return msg.replace(/\.+$/, '');
692+
};
693+
679694
const start = (msg = ''): void => {
680695
isSpinnerActive = true;
681696
unblock = block();
682-
_message = msg.replace(/\.+$/, '');
697+
_message = parseMessage(msg);
683698
process.stdout.write(`${color.gray(S_BAR)}\n`);
684699
let frameIndex = 0;
685700
let dotsTimer = 0;
686701
registerHooks();
687702
loop = setInterval(() => {
703+
if (isCI && _message === _prevMessage) {
704+
return;
705+
}
706+
clearPrevMessage();
707+
_prevMessage = _message;
688708
const frame = color.magenta(frames[frameIndex]);
689-
const loadingDots = '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
690-
process.stdout.write(cursor.move(-999, 0));
691-
process.stdout.write(erase.down(1));
709+
const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
692710
process.stdout.write(`${frame} ${_message}${loadingDots}`);
693711
frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0;
694712
dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0;
695713
}, delay);
696714
};
697715

698716
const stop = (msg = '', code = 0): void => {
699-
_message = msg ?? _message;
700717
isSpinnerActive = false;
701718
clearInterval(loop);
719+
clearPrevMessage();
702720
const step =
703721
code === 0
704722
? color.green(S_STEP_SUBMIT)
705723
: code === 1
706724
? color.red(S_STEP_CANCEL)
707725
: color.red(S_STEP_ERROR);
708-
process.stdout.write(cursor.move(-999, 0));
709-
process.stdout.write(erase.down(1));
726+
_message = parseMessage(msg ?? _message);
710727
process.stdout.write(`${step} ${_message}\n`);
711728
clearHooks();
712729
unblock();
713730
};
714731

715732
const message = (msg = ''): void => {
716-
_message = msg ?? _message;
733+
_message = parseMessage(msg ?? _message);
717734
};
718735

719736
return {

‎pnpm-lock.yaml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.