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