Skip to content

Commit

Permalink
feat(@clack/prompts): adapt spinner to CI environment (#169)
Browse files Browse the repository at this point in the history
Co-authored-by: Nate Moore <[email protected]>
  • Loading branch information
orochaa and natemoo-re authored Dec 14, 2024
1 parent a0e28ac commit f9f139d
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-moose-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clack/prompts': patch
---

Adapts `spinner` output for static CI environments
3 changes: 2 additions & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"scripts": {
"start": "jiti ./index.ts",
"spinner": "jiti ./spinner.ts"
"spinner": "jiti ./spinner.ts",
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts"
},
"devDependencies": {
"jiti": "^1.17.0"
Expand Down
36 changes: 36 additions & 0 deletions examples/basic/spinner-ci.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* This example addresses a issue reported in GitHub Actions where `spinner` was excessively writing messages,
* leading to confusion and cluttered output.
* To enhance the CI workflow and provide a smoother experience,
* the following changes have been made only for CI environment:
* - Messages will now only be written when a `spinner` method is called and the message updated, preventing unnecessary message repetition.
* - There will be no loading dots animation, instead it will be always `...`
* - Instead of erase the previous message, action that is blocked during CI, it will just write a new one.
*
* Issue: https://github.com/natemoo-re/clack/issues/168
*/
import * as p from '@clack/prompts';

const s = p.spinner();
let progress = 0;
let counter = 0;
let loop: NodeJS.Timer;

p.intro('Running spinner in CI environment');
s.start('spinner.start');
new Promise((resolve) => {
loop = setInterval(() => {
if (progress % 1000 === 0) {
counter++;
}
progress += 100;
s.message(`spinner.message [${counter}]`);
if (counter > 6) {
clearInterval(loop);
resolve(true);
}
}, 100);
}).then(() => {
s.stop('spinner.stop');
p.outro('Done');
});
2 changes: 1 addition & 1 deletion examples/basic/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as p from '@clack/prompts';
p.intro('spinner start...');

const spin = p.spinner();
const total = 10000;
const total = 6000;
let progress = 0;
spin.start();

Expand Down
33 changes: 25 additions & 8 deletions packages/prompts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,11 +643,13 @@ export const log = {
export const spinner = () => {
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
const delay = unicode ? 80 : 120;
const isCI = process.env.CI === 'true';

let unblock: () => void;
let loop: NodeJS.Timeout;
let isSpinnerActive = false;
let _message = '';
let _prevMessage: string | undefined = undefined;

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


const clearPrevMessage = () => {
if (_prevMessage === undefined) return;
if (isCI) process.stdout.write('\n');
const prevLines = _prevMessage.split('\n');
process.stdout.write(cursor.move(-999, prevLines.length - 1));
process.stdout.write(erase.down(prevLines.length));
};

const parseMessage = (msg: string): string => {
return msg.replace(/\.+$/, '');
};

const start = (msg = ''): void => {
isSpinnerActive = true;
unblock = block();
_message = msg.replace(/\.+$/, '');
_message = parseMessage(msg);
process.stdout.write(`${color.gray(S_BAR)}\n`);
let frameIndex = 0;
let dotsTimer = 0;
registerHooks();
loop = setInterval(() => {
if (isCI && _message === _prevMessage) {
return;
}
clearPrevMessage();
_prevMessage = _message;
const frame = color.magenta(frames[frameIndex]);
const loadingDots = '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
process.stdout.write(cursor.move(-999, 0));
process.stdout.write(erase.down(1));
const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
process.stdout.write(`${frame} ${_message}${loadingDots}`);
frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0;
dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0;
}, delay);
};

const stop = (msg = '', code = 0): void => {
_message = msg ?? _message;
isSpinnerActive = false;
clearInterval(loop);
clearPrevMessage();
const step =
code === 0
? color.green(S_STEP_SUBMIT)
: code === 1
? color.red(S_STEP_CANCEL)
: color.red(S_STEP_ERROR);
process.stdout.write(cursor.move(-999, 0));
process.stdout.write(erase.down(1));
_message = parseMessage(msg ?? _message);
process.stdout.write(`${step} ${_message}\n`);
clearHooks();
unblock();
};

const message = (msg = ''): void => {
_message = msg ?? _message;
_message = parseMessage(msg ?? _message);
};

return {
Expand Down
1 change: 1 addition & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f9f139d

Please sign in to comment.