🧩

自作CLI開発で使ってみたら便利だったUnJSライブラリの紹介

2024/08/06に公開

はじめに

TL;DR

CLI 開発で UnJS の unbuild、citty、consola、giget、pkg-types、jiti を使ってみたらめっちゃ便利だった。

本記事の内容と対象読者

本記事は、Vue.js v-tokyo Meetup #21にて筆者が LT 登壇した内容を記事化したものです。
筆者が最近取り組んでいた CLI 開発において UnJS を導入したところ、非常に便利と感じたため発表することとなりました。発表資料はGitHub Pages に上げています

https://vuejs-meetup.connpass.com/event/321431/

本記事では CLI 開発に便利な UnJS のライブラリをいくつか紹介し、どのように使ったのかの解説もします。UnJS に興味があり、どんなライブラリをどのように使うのか気になっている人を対象にしています。

前提知識として Node.js/TypeScript の基礎的な理解があると読みやすいでしょう。

検証環境

検証環境は下記の通りです。

  • Windows 10/11 Home
  • Node.js 18.17.1 / 20.x
  • pnpm 9.5

参考となるリポジトリ

筆者がメンテしている create-babylon-app では、v0.2 系から UnJS を使用しています。
ぜひコード例の参考としてご覧ください。

https://github.com/drumath2237/create-babylon-app

自作の CLI、create-babylon-app について

create-babylon-app とは、Babylon.js という Web 3D グラフィクスエンジンを使ったプロジェクトをローカル環境に作成できる CLI です。

https://www.babylonjs.com/

筆者は Babylon.js が好きで趣味でよく触っていたり、ユーザグループの運営をやっていたりしている関係でこの CLI を開発・メンテしています。
create-babylon-app はコマンドラインから実行し、いくつか質問に答えると回答に沿ったプロジェクトが作成される、いわゆる Scaffolding ツールです。
フロントエンドのツールだと、create-vite や Nuxt CLI などと動作が近く、実際に実装面でもかなり参考にしています。

cba

最近 create-babylon-app の v0.2 系をリリースし、その際に従来の技術スタックをごそっと UnJS に置き換えました。

https://github.com/drumath2237/create-babylon-app/releases/tag/v0.2.2

create-babylon-app のメンテは正直そんなにできていなかったのですが、最近モチベが高くなったので取り組んでいました。
詳細は Babylon.js ゆるほめ LT 会 vol.3 でお話しています。

そんな中で GANGAN さんのツイートを発見し、citty を中心に UnJS について調べることになりました。

https://twitter.com/gangan_nikki/status/1744908147733692742

UnJS とは

UnJS はあらゆる JavaScript フレームワーク・プラットフォームで動作するライブラリ・ツール群です。下記に公式の説明を引用します。

Agnostic Excellence: JavaScript Libraries, Tools, and Utilities, Crafted to Elevate Your Coding Journey.

https://unjs.io/

Nuxt の開発メンバーが中心となりメンテナンスされているらしく、実際に UnJS のライブラリの一部は Nuxt CLI などで利用されています。
UnJS のパッケージ一覧を見れば分かるのですが、「こういうのあると良いよね」というような便利ライブラリがすらりと並んでいて壮観です。執筆時点では 63 個が公開されていました。UnJS 何でも揃ってるじゃん......って定期的に言っていきたいですね。

unjs-packages
UnJS のパッケージ一覧ページ

https://unjs.io/packages

UnJS を使用して CLI の機能を実装する

ここからは、いくつかの UnJS のライブラリをユースケースと共にご紹介します。
今回、筆者が使用したライブラリは下記の 6 つとなります。

  • unbuild
  • citty
  • consola
  • giget
  • pkg-types
  • jiti

📦unbuild

https://unjs.io/packages/unbuild

unbuild はビルドツールの一種です。
unbuild を使う前は tsc で JS にコンパイルするだけだったのですが、将来的にバンドルなども考えたかったので unbuild を採用しました。
実際ビルドツール/バンドラは色んな選択肢がありますが、今回は UnJS を使う方針だったためノリで unbuild を使ってみたのと、CLI 実装の参考にしている Nuxt CLI が内部的に使っていたことが採用理由です。
https://github.com/nuxt/cli

簡単な Node.js・TypeScript なプロジェクトであれば、下記コマンドでいい感じに/dist以下へ.cjs/.mjsをビルドしてくれます。

pnpx unbuild

# もしくはローカルへインストールしたうえで
pnpm exec unbuild

ゼロコンフィグでも動きますが、ビルドの設定をしたい場合はbuild.config.tsを配置します。例えば create-babylon-app ではエントリポイントを指定しています。
declarationオプションを指定すると良い感じに型定義ファイルも出力してくれるので、結構いいですね。

build.config.ts
import { defineBuildConfig } from "unbuild";

export default defineBuildConfig({
  entries: ["./src/index.ts"],
});

詳しい設定は下記記事がわかりやすかったです。

https://www.memory-lovers.blog/entry/2024/07/28/082321

🌆citty

https://unjs.io/packages/citty

citty は CLI フレームワークの一種です。
いざ CLI を作ろうとしたとき、できるだけメインの機能実装に注力したいと考えますが、CLI にはコマンドライン引数のパースやサブコマンドの実装などメイン機能以外の実装もすることになるでしょう。そういう時に CLI フレームワークがあると便利です。

citty を使えば、コマンドライン引数の設定やサブコマンド、ヘルプの表示など、いわゆるな CLI であってほしい機能を簡単に実装できます。
公式の README にあるサンプルがわかりやすいので掲載します。

公式docsのサンプルコード
import { defineCommand, runMain } from "citty";

const main = defineCommand({
  meta: {
    name: "hello",
    version: "1.0.0",
    description: "My Awesome CLI App",
  },
  args: {
    name: {
      type: "positional",
      description: "Your name",
      required: true,
    },
    friendly: {
      type: "boolean",
      description: "Use friendly greeting",
    },
  },
  run({ args }) {
    console.log(`${args.friendly ? "Hi" : "Greetings"} ${args.name}!`);
  },
});

runMain(main);

🐨consola

https://unjs.io/packages/consola

consola はコンソールの出力をいい感じにしてくれたり、prompt による対話入力を受け付けたりできるライブラリです。
Node.js で標準出力をするときはconsole.logを使いますが、consola を使うことで出力に装飾を加えられます。

例えば下記の様なコードの場合、コンソールへの出力はこうなります。

consolaによる出力例
import { consola } from "consola";
import { colorize } from "consola/utils";
// ...
const msg = `Hello, ${name}`;
consola.info(msg);
consola.success(msg);
consola.fail(msg);
consola.start(msg);
consola.warn(msg);
consola.error({ message: "error", additional: "yeahh" });
consola.silent(msg);

consola.log(colorize("blue", "message blue"));
consola.log(colorize("yellow", "message yellow"));

alt text
consola での出力例

また、prompt によっていろんな種類の対話形式で入力を受け付けられます。
次のようなコードを書くと、動画のように対話式の入力を実現できます。

prompt入力のコード例
const name = await consola.prompt("What's your name?", {
  type: "text",
  initial: "Bob",
});

const isOk = await consola.prompt("Is this OK?", {
  type: "confirm",
  initial: true,
});

const mainJob = await consola.prompt("Main Job", {
  type: "select",
  options: [
    { label: "Student", value: "student" },
    { label: "Programmer", value: "programmer" },
    { label: "Other", value: "other" },
  ],
});

const foodSelection = await consola.prompt("Select your favorite", {
  type: "multiselect",
  options: [
    { label: "🍕Pizza", value: "pizza" },
    { label: "🍣Sushi", value: "sushi" },
    { label: "🍜Ramen", value: "ramen" },
  ],
});

https://youtu.be/Ohnp4-mXUxU

✨giget

https://unjs.io/packages/giget

giget は GitHub などのリモートリポジトリホスティング先からリポジトリの内容をダウンロードして、ローカルのディレクトリに展開できるツールです。
プロジェクトテンプレートを GitHub 上に保存して置いてそれをプログラム中でダウンロードしたい場合にとても便利です。それこそ、Nuxt CLI のような Scaffolding ツールにはもってこいですね。

例えば create-babylon-app では下記のコードを使ってテンプレートをダウンロードしています。

gigetを使ってGitHubリポジトリのフォルダをダウンロードする
import { downloadTemplate } from "giget";

const githubRepoUrlBase = "gh:drumath2237/create-babylon-app/templates";
const templateName = `${buildTool}-${language}`;
const { dir: appDir } = await downloadTemplate(
  `${githubRepoUrlBase}/${templateName}`, // A
  {
    dir: projectName, // B
    install: doInstall, // C
  }
);

コード中のAでは第 1 引数に GitHub のリポジトリ名を指定しています。加えてディレクトリ名を渡すことで特定ディレクトリのみをダウンロードできます。ブランチの指定も可能です。

コード中Bではダウンロードする先のフォルダ名を指定してます。
Scaffolding ツールではプロジェクト名を指定できる場合が多いので、そういう場合はプロジェクト名に従ってプロジェクトフォルダができてほしいですよね。

コード中Cでは依存パッケージのインストールをするか指定しています。
giget の内部では nypm という別ライブラリを使っています。これによりパッケージマネージャを自動判別して依存解決できるのです。すごいですね。
パッケージマネージャの判別ですが、自分の手元では yarn で若干挙動が怪しかったので、環境によっては正確に動作しない可能性があります。

🧃pkg-types

https://unjs.io/packages/pkg-types

公式ドキュメントには、次のような紹介文があります。

Node.js utilities and TypeScript definitions for package.json and tsconfig.json

まさにこの pkg-types というライブラリは、package.jsontsconfig.jsonの読み書きにおいて便利なユーティリティを提供しています。
JSON ファイルの読み書きなので自前で実装してしまっても良いのですが、もしそういう機能を提供しているいい感じのライブラリがあったら使いたいよね、というモチベーションで使える、痒い所に手が届くライブラリですね。

create-babylon-app ではテンプレートから生成したプロジェクトファイルのpackage.jsonにあるnameプロパティを変更したり、
create-babylon-app 自体のバージョン情報を--versionコマンドで表示したりする際に利用しています。

package.jsonnameプロパティを変更するコードを次に示します。

import { readPackageJSON, writePackageJSON } from "pkg-types";

const packageJson = await readPackageJSON(appDir);
if (packageJson.name) {
  packageJson.name = projectName;
  const jsonPath = path.resolve(appDir, "package.json");
  await writePackageJSON(jsonPath, packageJson);
}

🍟jiti

https://unjs.io/packages/jiti

jiti を使うことでランタイムや CLI で TypeScript のコードを実行できます。
ts-node や vite-node を使ったことがある人なら、ちょうどあのイメージですね。

残念ながら筆者は、jiti については本当に vite-node の代わりくらいにしか使っていないため、これ以上の説明は難しいです(すみません)。
CLI の開発をしているとき、その動作を確認するために毎回ビルドしているのでは時間がかかってしまいますので、ビルドする前の .ts ファイルをそのまま動かしたいというのが利用するモチベーションでしょうか。
次のように、ローカルインストールした jiti を動かすことができます。

pnpm exec jiti ./src/index.ts

おわりに

今回は筆者が CLI 開発において利用した UnJS のライブラリをご紹介しました。
ライブラリによっては合う合わないありますが、筆者のユースケースの場合はかなりマッチしているものが多く、「もう全部 UnJS でいいか......」みたいな気持ちになりました。
UnJS の中には、記事の中でご紹介出来ていないライブラリやツールがまだまだありますので、まだ使ったことないライブラリにも手を出してみたいところです。

最後まで読んでいただきありがとうございました。ハッピーな UnJS ライフを 👋。

参考文献

https://unjs.io

https://zenn.dev/ytr0903/articles/c6c42147ed29be

https://speakerdeck.com/drumath2237/create-cli-with-unjs

GitHubで編集を提案
Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion