Denoで社内ツールを作る
はじめに
社内業務を効率化するためのツールとして、キャッチアップも兼ねてDenoでCLIを作ってみました。
Cliffy
の使い方についてのメモや、Denoを触る上で迷ったところをまとめていきます。
Denoのインストール
まずはDenoをインストールします。
詳しいインストール方法は公式サイトを参照ください。
MacOSの場合は以下のコマンドでインストールできます。
brew install deno
もしくは
curl -fsSL https://deno.land/install.sh | sh
プロジェクトの作成
Denoではdeno init
コマンドでプロジェクトを作成できます。
deno init
# 実行結果
✅ Project initialized
Run these commands to get started
# Run the program
deno run main.ts
# Run the program and watch for file changes
deno task dev
# Run the tests
deno test
コマンドを実行するとdeno.json
というファイルが作成されます。
{
"tasks": {
"dev": "deno run --watch main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
このファイルは Node.jsの package.json
に相当するファイルで、
Denoのタスクの設定や依存パッケージの指定を行うことができます。
試しにdeno task dev
コマンドを実行してみます。
deno task dev
上記を実行すると、deno run --watch main.ts
が実行され、下記のような結果が表示されます。
deno task dev
Task dev deno run --watch main.ts
Watcher Process started.
Add 2 + 3 = 5
Watcher Process finished. Restarting on file change...
Cliffy
の導入
続いて、Cliffyを導入します。
CliffyはDenoでCLIツールを作成するためのフレームワークで、コマンド定義やオプション解析、ヘルプ生成などを簡単に行うことができます。
Denoではdeno add
コマンドで依存パッケージを追加できます。
deno add jsr:@cliffy/command@^1.0.0-rc.7
インストールしたいパッケージがnpm
にしかない場合は、
deno add npm:<package-name>
とすることでインストールできます。
例えば、zod
をインストールしたい場合は、
deno add npm:zod
とすることでインストールできます。
コマンドの作成
インストールが完了したら、main.ts
ファイルを下記のように編集します。
import { Command } from "@cliffy/command";
const cli = new Command()
.name("my-cli")
.description("My CLI Tool!")
.option("-m, --message <message:string>", "Message to use", { required: true })
.option("-n, --number <number:number>", "Number to use", { default: 1 })
.action((options) => {
const message = options.message;
const number = options.number;
console.log(`Hello, world! ${message} ${number}`);
});
cli.parse(Deno.args);
Cliffy
ではCommand
クラスに対して、メソッドチェーンでコマンドを定義できます。
例えば、.description
でコマンドの説明、.option
でオプションを定義、.action
でコマンドの処理を記述できます。
また、.option
で定義したオプションは、.action
の引数で受け取ることができます。
この状態でdeno run ./main.ts -h
コマンドを実行すると、下記のような結果が表示されます。
deno run ./main.ts -h
Usage: my-cli --message <message>
Description:
My CLI Tool!
Options:
-h, --help - Show this help.
-m, --message <message> - Message to use (required)
-n, --number <number> - Number to use (Default: 1)
デフォルトで追加された-h
オプションに加えて、
-m
や-n
オプションが追加されていることが確認できます。
実行してみると、下記のような結果が表示されます。
deno run ./main.ts -m world -n 10
Hello, world! world 10
また、.option
の引数に<message:string>
のように記述することで、オプションの型を推論してくれます。(すごい!)
試しに-n
オプションに数値以外の値を設定してみます。
deno run ./main.ts -m world -n hoge
Usage: my-cli --message <message>
Description:
My CLI Tool!
Options:
-h, --help - Show this help.
-m, --message <message> - Message to use (required)
-n, --number <number> - Number to use (Default: 1)
error: Option "--number" must be of type "number", but got "hoge".
正しくバリデーションが効いていることが確認できます。
サブコマンドの追加
続いて、サブコマンドを追加してみます。
サブコマンドの追加は、.command
メソッドを使うことで行えます。
import { Command } from "@cliffy/command";
const cli = new Command()
.name("my-cli")
.description("My CLI Tool!")
.option("-m, --message <message:string>", "Message to use", { required: true })
.option("-n, --number <number:number>", "Number to use", { default: 1 })
.action((options) => {
const message = options.message;
const number = options.number;
console.log(`Hello, world! ${message} ${number}`);
})
.command("sub", "Sub command")
.action(() => {
console.log("sub command");
});
cli.parse(Deno.args);
上記のようにメソッドチェーンで書くこともできますが、
読みづらい場合は、Command
インスタンスを受け取る形で、下記のようにコマンドを分けて記述することもできます。
import { Command } from "@cliffy/command";
const subCommand = new Command()
.name("sub")
.description("Sub command")
.action(() => {
console.log("sub command");
});
const cli = new Command()
.name("my-cli")
.description("My CLI Tool!")
.option("-m, --message <message:string>", "Message to use", { required: true })
.option("-n, --number <number:number>", "Number to use", { default: 1 })
.action((options) => {
const message = options.message;
const number = options.number;
console.log(`Hello, world! ${message} ${number}`);
})
.command("sub", subCommand);
cli.parse(Deno.args);
実行してみると、下記のような結果が表示されます。
deno run ./main.ts sub -h
Usage: my-cli sub
Description:
Sub command
Options:
-h, --help - Show this help.
CLIとして配布
作成したアプリケーションをCLIとして配布します。
配布先のユーザーがDenoのランタイムをインストールしている場合には、deno install
コマンドを実行することで、
--name
オプションで指定した名前でコマンドをインストールできます。
今回は下記のオプションで配布しました。
deno install --global --force --name my-cli --import-map deno.json ./main.ts
オプションを簡単に説明すると
-
--global
: グローバルにインストール -
--force
: すでに同名のコマンドが存在する場合に上書き -
--name
: コマンド名を指定 -
--import-map
: 外部依存パッケージがある場合に同梱する必要があるため、deno.json
(もしくはimport_map.json
)を指定
となっています。
インストール後、~/.zshrc
にパスを追記し、
ターミナルを再起動することで、
自作のCLIコマンドがどこでも使用できるようになります。
export PATH="$HOME/.deno/bin:$PATH"
source ~/.zshrc
# --name オプションで指定したコマンド名
my-cli -h
Usage: my-cli --message <message>
Description:
My CLI Tool!
Options:
-h, --help - Show this help.
-m, --message <message> - Message to use (required)
-n, --number <number> - Number to use (Default: 1)
Commands:
sub - Sub command
Denoのランタイムがない場合は、deno compile
コマンドでバイナリをビルドして配布します。
deno compile --target=aarch64-apple-darwin <必要に応じて`--allow-read`などのオプションを追加> main.ts
ビルドしたバイナリを/usr/local/bin
などパスが通っている場所に配置することで、
自作のCLIコマンドがどこでも使用できるようになります。
cp <your-binary-path> /usr/local/bin/<任意のコマンド名>
# /usr/local/bin/my-cli として配置した場合
my-cli -h
Usage: my-cli --message <message>
Description:
My CLI Tool!
Options:
-h, --help - Show this help.
-m, --message <message> - Message to use (required)
-n, --number <number> - Number to use (Default: 1)
Commands:
sub - Sub command
Denoを触る上で迷ったところ
以上でCLIのアプリケーションを作成・配布することができました。
ここからは、Denoを触る上で迷ったところをまとめていきます。
パッケージの管理をどうするか
Deno v1.x系では、公式のパッケージマネージャーが存在しなかったため、
deps.ts
やimport_map.json
というファイルを作成して
パッケージのインポートの記述をまとめて管理していました。
Deno v2からは、deno.json
のimports
に
{
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
のように記述することで、パッケージの管理を行うことができるようになりました。
一方で、deno install
時に依存関係のとして--import-map ./deno.json
のように指定すると、
下記のようにdeno.json
のimports
セクション以外に書かれているtasks
やcompilerOptions
に対してワーニングが出力されます。
Import map diagnostics:
- Invalid top-level key "tasks". Only "imports" and "scopes" can be present.
- Invalid top-level key "compilerOptions". Only "imports" and "scopes" can be present.
上記を避けるために、deno.json
のimports
の代わりに
{
"importMap": "./import_map.json"
}
のようにimportMap
を使用して、
deno install --import-map ./import_map.json
のようにJSONファイルの指定を分離できないか試してみました。
結果としては、deno install
自体はうまくいくものの、
importMap
を使用するとdeno add
コマンドが使えなくなる問題がありました。
deno add
が使えなくなるのは開発体験を損なうため、
deno.json
のimports
を使用してパッケージの管理を行い、deno install
時のワーニングは無視することにしました。
(deno.json
のimports
からimport_map.json
を作成するスクリプトを組めば解決するかもですが、そこまでする必要もないかと思いやりませんでした。)
deno.landとjsrのどちらを使うべきか
パッケージの管理についてはdeno.json
にまとめていく方針にしましたが、
さらにその中でdeno.land
とjsr
のどちらを使用するべきか迷いました。
具体的には、https
から始まるURLを使用するか、jsr:<package-name>
のようにjsr
を使用するかです。
{
"imports": {
"@cliffy/command": "https://deno.land/x/cliffy@v1.0.0-rc.7/command/mod.ts", // deno.land
"@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.7" // jsr
}
}
これについてはdeno.land
が今後大規模な開発が行われなくなるとのことで、基本的にはjsr
を使用するようにしました。
下記の記事がわかりやすかったため、詳細について気になる方はご参照ください。
まとめ
Denoをちゃんと触るのは初めてでしたが、
-
fmt
やlint
などがDenoに付随しており、様々なツールをインストールする必要がないこと - 使いやすいAPIが揃っていること
-
tsconfig.json
を作成せずに、すぐTypeScriptを実行できること
といった点で非常に楽に開発できました。
Discussion