unerry でインターンをさせていただいている dorimiamn です。
ご縁あって Advent Calndar の 13 日目を書かせていただいています。
普段は runn を用いた自動テスト周りを触らせていただいており、この記事でも runn のお話を書こうと思いましたが、荷が重たかったのでこちらは見送りました。
その代わり、この記事ではプロダクトのパフォーマンスを改善するため、ローカル環境で大量のデータを投入したいときに適当な TypeScript 製 CLI ツールを作ってみた際の経験を備忘録としてまとめてみます。
見た目が綺麗で簡単めな CLI ツールを作るのに使えるライブラリを紹介しつつ適当なものを 1 つ作ってみようかと思います。
完成品
コードの全体像はこちらに挙げています。
実行環境
JS の Runtime は Bun を使います。
利用するライブラリ
目玉のライブラリは以下の 3 つです。
consola
1 つ目は JS 標準の console の wrapper です。
console を使う感覚でリッチ目な表現をしてくれます。
import consola from "consola";
consola.log("Hello Consola!");
consola.warn("Warn Text");
consola.error("Error Text");
上記のように書くと以下のように表示されます。
時刻付きな上、warn や error の場合には白抜き文字で強調して表示してくれます。
これ以外のメソッドもあり、例えばテキストの先頭に記号を付けてログを見やすくもできます。
以下のように書くと、
consola.info("Info Text");
consola.start("Start Text");
consola.success("Success Text");
下記の画像のような表示が出ます。
ora
こちらは Spinner という、Web サイトでコンテンツ読み込み中によく表示されるぐるぐるするアニメーションを簡単にターミナルに表示できます。
例として以下のようなコードを動かしてみます。
import ora from "ora";
const NUM = 100
const spinner = ora("処理を開始します").start();
let completed = 0;
for (let i = 0; i < NUM; i++) {
// 何らかの処理
await Bun.sleep(100);
completed++;
spinner.text = `実行中... (${completed}/${NUM})`;
}
spinner.succeed("処理が完了しました");
上記のコードを実行してみると以下のようなアニメーションが動いてくれます。
ローカルで大量に非同期な処理をしたいときに使うと進捗状況が確認できて便利です。
アニメーションがあるので動いてるな〜というのがちゃんとわかるので安心して動作を見てられます。
p-queue
これは 複数の Promise を扱いやすくしてくれるライブラリです。
公式 README.md に書いてあるとおり、Promise の Queue なので p-queue ですね。
Promise queue with concurrency control
便利なところとして、Promise の同時実行数を手軽に指定して制御できるのが良いです。
先ほどの ora と組み合わせて以下のように 100 個の Promise 最大 20 個まで並列に実行させつつ完了を待つなんてことができます。
import consola from "consola";
import ora from "ora";
import PQueue from "p-queue";
const NUM = 100;
const CONCURRENCY_NUM = 20;
consola.info(`並列処理のキューイングを開始します。総タスク数: ${NUM}`);
consola.info(`同時実行数の上限: ${CONCURRENCY_NUM}`);
const spinner = ora("処理を開始します").start();
await Bun.sleep(1000); // スピナーの開始を視覚的に確認するための待機
let completed = 0;
const parallelRequestQueue = new PQueue({
autoStart: true,
concurrency: CONCURRENCY_NUM,
});
// テストデータの投入
for (let i = 0; i < NUM; i++) {
parallelRequestQueue.add(async () => {
// 後の待機ほど時間を伸ばすことで同時実行している様子を可視化
await Bun.sleep(1000+i*10);
completed++;
spinner.text = `実行中... (${completed}/${NUM})`;
});
}
await parallelRequestQueue.onIdle();
spinner.succeed("処理が完了しました");
このコードを実行したアニメーションは以下の通りになります。
コード中で、待機時間を 1000+i*10 ミリ秒とすることで完了時間を若干ばらけさせているため、アニメーション中でも 20、40、60 で数字の増え方が一旦止まりながら増えていく様子が確認できます。
この p-queue を使うと、ローカルの開発環境で雑に API を大量に叩きたいときや大量のファイルを読み書きする処理を実装したいときに自前で並列実行数関連の制御を行わずに済むので楽です。
使用例
ここまで紹介したスクリプトを組み合わせるとこんなことができるよっているデモとして、Hono でローカルに立てた Server へリクエストを大量に送って負荷テストやパフォーマンステストを簡単にできるようにするつもりでした。
しかし、実装量がそこそこ必要でデモとして作るのに時間が足りないのでファイル I/O でお茶を濁します。
JSON ファイルを作成して読み込むコード
どこで使うかは分かりませんが、デモなのでこれをよしなに改変すれば他にも応用できるはずです。
画像処理や Markdown の変換とかには役立ち気がします。
動きとしては、
- 適当な値を持った JSON ファイルを 1,000 件単位で作成。データとして乱数で年齢を生成
- 作成した JSON ファイルを読み込む
- 読み込んだ JSON ファイルにある年齢を集計して平均を計算する
という流れです。
import consola from "consola";
import ora from "ora";
import PQueue from "p-queue";
type JSON_CONTENT_TYPE = {
id: number;
name: string;
age: number;
};
const FILE_NUM = 1000;
// ファイルの読み書きを 1,000 件 order でやる。
// 10000 件ファイルを作成する
async function createJSONFile(index: number) {
const jsonObject = {
id: index,
name: `User_${index}`,
age: Math.floor(Math.random() * 100),
};
const jsonContent = JSON.stringify(jsonObject, null, 2);
await Bun.write(`./files/file_${index}.json`, jsonContent);
await Bun.sleep(Math.random() * 500); // I/O に時間がかかっているふうにする
}
async function createFiles(fileNum: number) {
const queue = new PQueue({ concurrency: 20 });
const spinner = ora("ファイル作成を開始します").start();
let completed = 0;
const jobs: (() => Promise<void>)[] = [];
for (let i = 0; i < fileNum; i++) {
jobs.push(async () => {
await createJSONFile(i);
completed++;
spinner.text = `ファイル作成中: ${completed}/${fileNum}`;
});
}
queue.addAll(jobs);
await queue.onIdle();
spinner.succeed("ファイル作成が完了しました");
}
async function readJSONFile(index: number) {
const content = await Bun.file(`./files/file_${index}.json`).text();
const jsonContent: JSON_CONTENT_TYPE = JSON.parse(content);
await Bun.sleep(Math.random() * 500); // I/O に時間がかかっているふうにする
return jsonContent;
}
async function calcAverage() {
consola.info("平均年齢を計算します");
const queue = new PQueue({ concurrency: 20 });
const spinner = ora("ファイル読み込みを開始します").start();
let completed = 0;
const jobs: (() => Promise<void>)[] = [];
let totalAge = 0;
for (let i = 0; i < FILE_NUM; i++) {
jobs.push(async () => {
const { age } = await readJSONFile(i);
totalAge += age;
completed++;
spinner.text = `ファイル読み込み中: ${completed}/${FILE_NUM}`;
});
}
queue.addAll(jobs);
await queue.onIdle();
spinner.succeed("ファイル読み込みが完了しました");
consola.info(`平均年齢: ${totalAge / FILE_NUM}`);
}
await createFiles(FILE_NUM);
await calcAverage();
これを実際に動かしてみた動画が以下の通りです。
スピナーが動いているだけで楽しくなれます。
処理が正常に動いているだけでやはり安心感があるのもいいですね。
終わりに
TypeScript でも簡単に CLI ツールをリッチにできるライブラリを簡単に紹介しました。
追加で対話型インタフェースの作成に便利な Inquirer というライブラリも紹介したかったですが、今回扱う範囲だとそこまでしなくてもなんとかなったので出番が無く……。
もしもっと複雑なものを作成する場合はこちらを使うと嬉しいかもしれません。
TypeScript でリッチな動きのある CLI ツールを作ってみたい!というときの参考になれば嬉しいです。
気が向いたら Enquirer を使った CLI ツールを作ってみるかもしれません。




