くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

cittyとunbuildでCLIツールを作ってみた(テンプレート付き)

以前、unjs/unbuildに入門したけど、
cittyもつかってオレオレCLIツールを作ってみたときの備忘録(*´ω`*)

テンプレートとしても使える、サンプルリポジトリもここにつくっておいた

github.com

cittyとunbuild

どちらもUnJsのパッケージで、

この2つを使えば、ちょっとしたライブラリやCLIを簡単に作れる(*´ω`*)

cittyの使い方

使い方はシンプルで、こんな感じ。

// main.ts
import { runMain, defineCommand } from "citty";
import { description, name, version } from "../package.json";

// コマンドの定義
const main = defineCommand({
  // コマンドの名前、バージョンなどの情報
  meta: { name, version, description },
  // 引数に関する定義
  args: {
    text: {
      type: "positional",
      description: "base text",
      required: true,
    },
    prefix: {
      type: "string",
      alias: "p",
      description: "preffix text",
      default: "Hello, ",
    },
    quote: {
      type: "boolean",
      alias: "q",
      description: "append quotation",
      default: false,
    },
  },
  // 実際に実行する処理
  run: async ({ args }) => {
    // argsには、コマンドの引数が
    // 上のargs{}の形式で渡されてくる
  },
});

// 定義したCLIコマンドを実行
runMain(main);

とりあえず、-hをつけてjitiで実行してみると、

$ npx jiti src/main.ts -h
Template for CLI and npm package powered by citty and unbuild. (template-citty-unbuild v0.0.0)                                            

USAGE template-citty-unbuild [OPTIONS] <TEXT>

ARGUMENTS

  TEXT    base text    

OPTIONS

  -p, --prefix="Hello, "    preffix text    
             -q, --quote    append quotation

defineCommandで指定した内容からヘルプを出してくれる

引数の定義

argsまわりは、こんな感じっぽい

// 引数に関する定義
args: {
  text: {
    // positionalは、フラグなしの引数
    type: "positional",
    description: "base text",
    // requiredをつけると必須になる
    required: true,
  },
  prefix: {
    // stringは、--<名前>=<文字列>の引数
    type: "string",
    // -pのように、ショート版も指定できるただし、1文字のみ
    alias: "p",
    description: "preffix text",
    // 省略された場合の、デフォルト値も指定できる
    default: "Hello, ",
  },
  quote: {
    // booleanは、--<名前>=<真偽値>の引数
    type: "boolean",
    alias: "q",
    description: "append quotation",
    default: false,
  },
},

defuを使ってデフォルト値とマージする

defaultにデフォルト値を書く以外にも、
defuを使ってマージするとかもできる

import { defineCommand } from "citty";
import defu from "defu";
import { description, name, version } from "../package.json";

// オプションの型定義
interface Options {
  prefix: string;
  suffix: string;
  quote: boolean;
}

// オプションのデフォルト値
const DEFAULT_OPTION: Options = {
  prefix: "Hello, ",
  suffix: "",
  quote: false,
};

const main = defineCommand({
  // ...
  run: async ({ args }) => {
    // defuを使って、undefinedの引数に、デフォルト値を設定する
    const options: Options = defu(args, DEFAULT_OPTION);
    // ...
  },
});

CLIを含めてunbuildする

npm packageとして公開できるように、
いろいろ設定していく

ディレクトリ構成

.
├── bin/
│   └── cli.mjs      ... CLIコマンド
├── src/
│   ├── lib/         ... ライブラリのコード
│   ├── cli.ts       ... CLIコマンドの定義
│   ├── index.ts
│   └── types.ts
├── build.config.ts  ... "unbuild"の設定ファイル
└── package.json

build.config.ts

unbuild用の設定ファイルを用意

// build.config.ts

import { defineBuildConfig } from "unbuild";

export default defineBuildConfig([
  {
    // エントリーポイントの指定。index.tsを起点にビルドする
    entries: [ 'src/index.ts' ],
    // 型定義も出力する
    declaration: true,
    rollup: {
      // デフォルトはmjsのみなので、cjsも出力する
      emitCJS: true,
      // msj/cjsをminifyする
      esbuild: { minify: true }
    },
  },
]);

src/cli.ts

CLIコマンドの定義はこんな感じで用意

// src/cli.ts
import { runMain as _runMain, defineCommand } from "citty";
import { description, name, version } from "../package.json";

const main = defineCommand({
  meta: { name, version, description, },
  args: {
    // 略
  },
  run: async ({ args }) => {
    // 略
  },
});

// CLIの実行を関数化してexportしておく
export const runMain = () => _runMain(main);

src/index.ts

exportするものをまとめたindex.tsはこんな感じ。
unbuild.config.tsentriesに指定したファイル

// src/index.ts

// CLIコマンド
export { runMain } from "./cli";
// ライブラリの関数とか
export { getMessage } from "./lib/message";
// 型定義もあれば
export type { Options } from "./types";

bin/cli.mjs

npxなどで実行するファイルを用意。
dist/index.mjsとかだとシバン(shebang/1行目)がないので、うまく動かない。。

#!/usr/bin/env node

import { runMain } from "../dist/index.mjs";

runMain();

package.json

nameversiondescription以外で、
設定が必要なのはこのあたり

{
  "exports": {
    ".": {
      // ES Module向けのexports
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      // CommonJS向けのexports
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    }
  },
  // binのcliファイルを設定
  "main": "./bin/cli.mjs",
  // binには、コマンド名と対応するファイルを設定
  "bin": {
    "template-citty-unbuild": "./bin/cli.mjs"
  },
  // 型定義ファイルも設定
  "types": "./dist/index.d.ts",
  // ビルドしたファイル(dist)とbinを公開するように設定
  "files": [
    "dist",
    "bin"
  ],
  // このパッケージ自体はCommonJSにしておく
  "type": "commonjs",
  // 略
}

ビルドしてみる

これでnpx unbuildを実行すると、
./dist配下に以下のファイルが生成される

./dist/
├── index.cjs
├── index.d.cts
├── index.d.mts
├── index.d.ts
└── index.mjs

ビルドしたあとに、./bin/cli.mjsで実行できればOK
node ./dist/index.mjsとかでも確認できる

あとは、パッケージをnpmjsGitHub Packagesとかで公開すれば使えるように(*´ω`*)

昔書いた記事はこちらに〜


以上!! テンプレートも作ったので、いろいろ捗るぞ...(*´ω`*)!!

参考にしたサイト様