Hiratake Web ロゴ

citty で CLI ツールをつくってみる

投稿した日
更新した日
書いたひと
icon
ひらたけ

このウェブサイトでも使用している Vue.js のフレームワーク Nuxt。 先月リリースされた [email protected] に以下のような記述がありました。

We've refactored nuxi using unjs/citty and this marks the first Nuxt release that depends on the new version, safely in its own repository.

Google 翻訳にそのまま入れてみると、nuxi(Nuxt CLI)を unjs/citty というものを使って作り直したよ的な話っぽい。なんとなく気になったので、unjs/citty を触ってみることにしました。

エレガントな CLI ビルダーらしい。記事執筆時点では citty の詳細なドキュメントやテンプレートのようなものはなく(issue を見ると将来的には用意するのを予定しているっぽいですが)、UnJS の他のリポジトリで結構使われていたので、そちらの実装を参考に動かしてみることに。

環境

今回作業した環境は以下のとおりです。

GitHub の UnJS オーガニゼーションのリポジトリ一覧でたまたま上の方にあった unjs/untun とか unjs/mkdist とかを参考に進めます。

root/
 ├ bin/
 │ └ command.mjs
 ├ src/
 │ ├ cli.ts
 │ ├ command.ts
 │ └ index.ts
 ├ package.json
 └ tsconfig.json

必要なパッケージをインストールする

まず必要なパッケージをインストールします。エレガントな CLI ビルダーである citty、記述した TypeScript のコードをビルドする unbuild をインストールします。

$ pnpm add citty
$ pnpm add -D unbuild

unjs/unbuild は、package.json に記述した情報から自動で構成を推測して、いい感じにビルドしてくれるものっぽい。citty を使用している他のリポジトリを参考に、以下のように package.json を変更します。

{
  ...
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.cjs",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    },
    "./cli": {
      "require": "./dist/cli.cjs",
      "import": "./dist/cli.mjs",
      "types": "./dist/cli.d.ts"
    }
  },
  "bin": {
    "command": "./bin/command.mjs"
  },
  "files": ["dist", "bin"],
  "dependencies": {
    "citty": "0.1.4"
  },
  "devDependencies": {
    "unbuild": "2.0.0"
  },
  "scripts": {
    "build": "unbuild"
  }
}

これで、src にコードを書いて、TypeScript の場合は tsconfig.json で設定をいい感じに書いて、 pnpm build を実行すると、dist/ ディレクトリにいい感じにファイルを作ってくれるっぽい。便利。

コマンドを実装する

CLI ツールなので、コマンドを実行すると何かしらの処理が実行されるプログラムを用意する必要があります。今回はとりあえず、文字列が表示されるだけの簡単なものを用意します。

src/command.ts に、コマンドを実行したときの処理を実装します。コンソールにただ「fuga」と出るだけの hoge 関数です。

export const hoge = () => {
  console.log('fuga')
}

src/index.ts には、CLI ではなく import {} from 'command' みたいにインポートして使うとき用に関数をそのままエクスポートする記述を入れておきます。

export * from './command'

続いて、CLI で実行するときの設定を src/cli.ts に書きます。defineCommand という関数でコマンドを定義し、そのコマンドを runMain という関数で実行するっぽい。

import { defineCommand, runMain } from 'citty'
import { hoge as hogeCommand } from './command'

export const hoge = defineCommand({
  meta: { name: 'hoge' },
  run: () => hogeCommand(),
})

export const main = defineCommand({
  meta: {
    name: 'command',
    version: '1.0.0',
  },
  subCommands: { hoge },
})

export const runCommand = () => runMain(main)

最後に、エクスポートした runCommand 関数を実行する用のファイルを bin/command.mjs に作成します。

#!/usr/bin/env node
import { runCommand } from '../dist/cli.mjs'
runCommand()

ビルドする

書いたコードをビルドします。ビルドを実行すると、dist/ に必要なファイルが生成されます。

$ pnpm build

試しに今回作成した CLI ツールを、他のプロジェクトでインストールして実行してみると、設定した文字列が出力されることが確認できます。

$ mkdir -p ../example
$ cd ../example
$ pnpm add [作成したCLIツールのパス]
$ pnpm [作成したCLIツールのパッケージ名] hoge

--help オプションをつけるとよくある感じでコマンドのヘルプが表示されたり、--version オプションをつけるとバージョンが出力されたりと、特に処理を書くとかしなくても用意してくれるのは便利だなと思いました。

今は特につくりたいなあと思うようなものが思いつかないですが、何かつくりたさはあるなあ。