Readline | Node.js v14.4.0 Documentation
readline モジュールは、可読ストリーム (process.stdin など) から 1 行ずつデータを読み込むためのインターフェイスを提供してくれます。標準出力にももちろん対応しており、これを用いることでローディングスピナーをパパっと作れます。紹介するのは最小構成的なモノでより深く学ぶにはGitHubでがっつり作られた似た目的のライブラリを読むことがおすすめです。
コードは次です。
const rl = require('readline');
class Spinner {
protected spinChars: Array<string>;
protected spinCount = 0;
public message = '';
constructor(spinChars: Array<string> | null = null) {
this.spinChars = spinChars || [
'⠋',
'⠙',
'⠹',
'⠸',
'⠼',
'⠴',
'⠦',
'⠧',
'⠇',
'⠏',
];
}
/**
* ローディングスピナーを回す
*/
spin() {
process.stdout.write('\x1B[?25l'); // カーソルを非表示にする
// 重要な部分
// 指定された方向に現在の行をクリアします。
// -1: 左, 0: 行全体, 1: 右
rl.clearLine(process.stdout, 0); // 行をすべて削除
// カーソルを移動します. 第二引数は十分に大きな負の値.
// todo 第三引数の制御と改行で複数のスピナーを上手く走らせられる、はず
rl.moveCursor(process.stdout, -100, 0); // 一番左側に戻る
// ローディングスピナーの描画と現在のメッセージの表示
// 改行をすると管理する範囲が広がって大変面倒なのでconsole.logでなく、これ
process.stdout.write(`${this.spinChars[this.spinCount]} ${this.message}`);
// 重要な部分ここまで
// スピナーのコマを進める
this.spinCount += 1;
if (this.spinCount >= this.spinChars.length) {
this.spinCount = 0;
}
}
/**
* ローディングスピナーを回しているIntervalId
* @return {number}
*/
run() {
return setInterval(() => {
this.spin();
}, 50);
}
}
/**
* 実行例
*/
let counter = 20;
const spinner = new Spinner();
spinner.message = `${counter}`;
const spinnerIntervalId = spinner.run();
const msgUpdateIntervalId = setInterval(() => {
counter -= Math.random();
spinner.message = `${counter}`;
if (counter <= 0) {
clearInterval(spinnerIntervalId);
clearInterval(msgUpdateIntervalId);
rl.clearLine(process.stdout, 0); // 行をすべて削除
process.stdout.write('\nfinish.\n');
process.stdout.write('\x1B[?25h'); // カーソルを表示にする
}
}, 100);
実行結果が次です。

clearLine と moveCursor で使った行を消して、カーソル(文字列を出力する始点)を初期化し続けるのが肝です。
Readline | Node.js v14.4.0 Documentation
Readline | Node.js v14.4.0 Documentation
あとは一コマ一コマ別の文字をスピナーとして決めて、適当なメッセージと合わせて表示するだけです。例のコードでは再利用も考えてクラス化しましたが、書き捨てスクリプトに入れるだけなら次の様な出力部を繰り返すだけでも同じ動作を作れます。
rl.clearLine(process.stdout, 0); // 行をすべて削除
rl.moveCursor(process.stdout, -100, 0); // 一番左側に戻る
process.stdout.write(`${this.spinChars[this.spinCount]} ${this.message}`);