tl;dr
- Denoには
deno compileが標準搭載されており、追加ツールなしでTypeScriptを単一実行ファイルにできる -
main.tsを書いてdeno testでテストし、deno task compileでビルドするだけの最短ルートを解説 - 生成物にはDenoランタイムが同梱されるので、配布先にDenoがなくても動く
対象読者
- Denoを触ったことがない、または触り始めたばかり
- TypeScriptの基本文法がわかる
- シェル(黒い画面)でコマンド操作ができる
本記事の執筆には生成AIを利用しています
Denoには標準で単一実行ファイルを生成する deno compile が組み込まれています。
Node.jsのように pkg や nexe などの外部パッケージを別途インストールする必要がなく、TypeScriptのまま書いたコードをそのままバイナリ化できます。
- ランタイム同梱: 生成した実行ファイルはDeno本体を内包するため、実行環境にDenoがなくても動く
- TypeScriptネイティブ: トランスパイル設定を書かなくてもそのままコンパイル可能
-
クロスコンパイル対応:
--targetオプションで他OS向けバイナリも生成できる
この記事では、実際に手を動かしながら Hello, world! を出力するCLIを作り、単一実行ファイルにビルドするところまでをひととおり体験します。
Step 1: プロジェクトを作る
作業用ディレクトリを作り、deno.json を用意します。
mkdir deno-cli && cd deno-cli
deno init
deno init で生成されるファイル構成は以下のとおりです。
deno-cli/
├── deno.json
├── main.ts
└── main_test.ts
Step 2: Hello, world! を出力するソースを書く
main.tsに処理本体を書きます。import.meta.mainはこのファイルが直接実行されたときだけtrueになるため、モジュールとしてインポートされたときは何も実行されません。
export function hello(): string {
return "Hello, world!";
}
if (import.meta.main) {
console.log(hello());
}
書けたら、一度そのまま実行して動作を確認します。
$ deno run main.ts
Hello, world!
Step 3: lintをかける
Denoは標準でリンターを内蔵しているため、追加ツールなしでコードの静的解析ができます。
$ deno lint
Checked 1 file
Step 4: fmtをかける
フォーマッターも標準搭載です。整形が必要な箇所があれば自動で直してくれます。
$ deno fmt
Checked 1 file
Step 5: テストを書く
Denoは標準でテストランナーも内蔵しているため、追加ライブラリなしでテストが書けます。アサーションだけ @std/assert を使います。
import { assertEquals } from "@std/assert";
import { hello } from "./main.ts";
Deno.test("returns Hello, world!", () => {
assertEquals(hello(), "Hello, world!");
});
deno.json の imports に依存を定義しておきます。
{
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
テストを実行します。
$ deno test
running 1 test from ./main_test.ts
returns Hello, world! ... ok (756µs)
ok | 1 passed | 0 failed
Step 6: タスクを定義する
毎回長いコマンドを打たずに済むよう、deno.json の tasks に開発・テスト・ビルドの3コマンドをまとめておきます。
{
"tasks": {
"dev": "deno run main.ts",
"compile": "deno compile --output hello main.ts",
"test": "deno test"
}
}
-
deno task dev… ソースをそのまま実行 -
deno task test… テストを実行 -
deno task compile… 単一実行ファイルにビルド
deno lint と deno fmt はDenoに標準搭載されているため、ESLintやPrettierを別途インストールしなくてもすぐ使えます。
頻度が低く省略にもならないコマンドになるので今回はtasksに含めず、直接実行する運用にしています。
Step 7: 単一実行ファイルにビルドする
deno task compile で main.ts をバイナリ化します。
$ deno task compile
Compile main.ts to hello.exe
生成された hello.exe(Windowsの場合)を実行します。
$ ./hello.exe
Hello, world!
Denoがインストールされていない環境でも、この実行ファイル単体で動作します。配布時にランタイムの
インストール手順を案内する必要がないのは地味に嬉しいポイントです。
おまけ: cloneせずGitHubから直接実行する
deno run にはURLを渡せるため、リポジトリをcloneしなくても以下のようにGitHub上のソースを直接実行できます。
$ deno run https://raw.githubusercontent.com/hidao80/deno-cli/main/main.ts
Hello, world!
この例ではgithub.com のファイル閲覧ページのURL(blobリンク)ではなく、raw.githubusercontent.com のURLを指定する点に注意してください。
ハマりポイント
-
importパスとtasksのパスがずれる: ファイルを
src/配下などに移動すると、main_test.tsのimportパスとdeno.jsonのtasks側パスを両方直す必要があります。片方だけ直すとテストかビルドの一方だけが壊れて気づきにくいので、移動したら両方grepして確認するのが安全です -
初回コンパイルは時間がかかる:
deno compileをはじめて実行すると、対象OS向けのDenoランタイム本体(denort)が ダウンロードされます。2回目以降はキャッシュされるため高速です -
Windowsでは拡張子が自動で付く:
--output helloと指定しても、Windows上ではhello.exeが生成されます。 README等でパスを案内する際はOS差を意識しておくと親切です -
deno fmtはMarkdownも整形対象: デフォルトでは.mdファイルも既定の折り返し幅で整形されてしまいます。 記事本文まで意図せず改行されて困る場合は、deno.jsonに以下のようにfmt.excludeを設定して除外します
{
"fmt": {
"exclude": ["*.md"]
}
}
ここで紹介した内容の公開リポジトリ
まとめ
ここまでで、Hello, world! を出力するだけのシンプルなCLIをDeno + TypeScriptで書き、テストを通し、deno compile で単一実行ファイルにするまでを体験しました。
外部ツールなしでバイナリ配布まで完結するのがDenoの強みです。小さなCLIツールを手軽に配布したいときの選択肢として選べるようにしておくと便利ですね。