Denoの依存モジュールの一元管理とバージョンのロック
こんにちは、ナカエです。本日はDenoについての記事です。
DenoはNode.jsの作者Ryan Dahlによって作られた、JavaScript/TypesScriptのモダンな実行環境です。
Deno 1.25でのnpmパッケージの試験的サポート により、Denoの当初の理想からは少し外れるものの、npmのエコシステムの力を借りて本格的な普及への道が見えてきたかなと個人的に感じています。
以下ではDenoの依存モジュールの管理について見ていきます。
環境
- OS: macOS Monterey 12.6
- CPU: Apple M1 Max
- zsh 5.8.1
- Homebrew 3.6.5
- Deno 1.26.1
- TypeScript 4.8.3
HTTPサーバでHello world
まずはDenoをインストールします。 MacであればHomebrewでインストールできます。
参考: Insallation
brew install deno
Denoのプロジェクトの初期化には deno init
コマンドを使ってもよいのですが、最低限TypeScriptファイルが一つあれば動くので今回は手動で作成していきます。
作業用のディレクトリを作成し、
mkdir deno_lock
cd deno_lock
続いてmain.tsを作成します。
Deno公式サイトのトップページにあるGetting started のとおりの、Hello worldをレスポンスとして返すHTTPサーバです。
標準のhttpモジュールを利用しています。
main.ts
import { serve } from "https://deno.land/std@0.159.0/http/server.ts";
serve(req => new Response("Hello World\n"));
ファイルを保存したら早速実行してみます。
deno run --allow-net main.ts
依存モジュールがダウンロードされ、
Listening on http://localhost:8000/
とコンソールに出力されます。 ブラウザで http://localhost:8000 にアクセスすればHello Worldのテキストが表示されます。
DenoはNodeでの開発経験から得られた知見の一つとしてデフォルトでの安全性を志向しており、特に指定しなければDenoモジュールからのファイル・ネットワークへのアクセスができないようになっています。 –allow-net はネットワークアクセスの許可を与えるオプションです。
Denoの依存関係の管理
ここまででNode.jsを使った開発で見慣れたpackage.jsonに相当するファイルが登場しませんでした。
Denoでは外部モジュールもローカルモジュールに直接インポートされるため、npmのようなランタイム本体とは別立てのパッケージマネージャが存在しません。 外部モジュールのimport元の指定はURLであり、実行時にモジュールの依存関係が解決され、必要な外部モジュールをダウンロードしてからコンパイルが行われるようになっています。
参考:Import and Export Modules | Manual | Deno
簡潔で賢い仕組みですね。
モジュールの一元管理
import文を書くだけで済むと聞くと良いことづくめのように感じますが、一つ落とし穴があります。
今回のように1ファイルで済むようなケースではなく、複数のファイルから同じモジュールのURLを指定してimportを行う場合を考えます。 そのモジュールのバージョンを上げるためには複数のファイルのimport文を修正する必要があり、特にプロジェクトが大規模になった場合に依存モジュールのバージョン管理が非常に面倒になるのは想像に難くありません。
Denoではこの問題を解決するために、deps.ts ファイルを作成し、それをプロキシとして外部モジュールを参照する方法が推奨されています。
参考: Managing Dependencies | Manual | Deno
今回の場合はdeps.tsにhttpモジュールのserve関数をdeps.tsで取り込み、exportします。
deps.ts
export { serve } from "https://deno.land/std@0.159.0/http/server.ts";
main.tsも合わせてローカルモジュールからのimportに書き換えます。
main.ts
import { serve } from "./deps.ts";
serve(req => new Response("Hello World\n"));
パッケージマネージャが陽には存在しないものの、npmのpackage.jsonの担っていた役割の一部をdeps.tsに担わせることは必要に応じて可能、と捉えておけば良いと思います。
ロックファイルと整合性チェック
さて、続いてモジュールのバージョンのロックについてです。
こちらはNodeでのpackage-lock.jsonによるパッケージのバージョンロックに相当する機能となります。
本番環境でアプリケーションを実行する場合には必須の機能と言ってもいいでしょう。
参考:Integrity Checking & Lock Files | Manual | Deno
deno cache コマンドで–lock、–lock-writeオプションを指定して実行すると、
deno cache --lock=lock.json --lock-write main.ts
lock.jsonが生成されます。
lock.jsonにモジュールごとのコミットハッシュが記録されていることを確認できます。
{
"https://deno.land/std@0.159.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06",
"https://deno.land/std@0.159.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0",
"https://deno.land/std@0.159.0/async/debounce.ts": "de5433bff08a2bb61416fc53b3bd2d5867090c8a815465e5b4a10a77495b1051",
"https://deno.land/std@0.159.0/async/deferred.ts": "c01de44b9192359cebd3fe93273fcebf9e95110bf3360023917da9a2d1489fae",
"https://deno.land/std@0.159.0/async/delay.ts": "0419dfc993752849692d1f9647edf13407c7facc3509b099381be99ffbc9d699",
"https://deno.land/std@0.159.0/async/mod.ts": "dd0a8ed4f3984ffabe2fcca7c9f466b7932d57b1864ffee148a5d5388316db6b",
"https://deno.land/std@0.159.0/async/mux_async_iterator.ts": "3447b28a2a582224a3d4d3596bccbba6e85040da3b97ed64012f7decce98d093",
"https://deno.land/std@0.159.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239",
"https://deno.land/std@0.159.0/async/tee.ts": "d27680d911816fcb3d231e16d690e7588079e66a9b2e5ce8cc354db94fdce95f",
"https://deno.land/std@0.159.0/http/server.ts": "e99c1bee8a3f6571ee4cdeb2966efad465b8f6fe62bec1bdb59c1f007cc4d155"
}
一度lock.jsonを生成しておけば、lock.jsonに記録されたバージョンを元に依存モジュールをマシンにキャッシュすることが可能です。
deno cache --reload --lock=lock.json main.ts
また、deno runによる実行の際に–lockを指定することもできます。
deno run --lock=lock.json --allow-net main.ts
さらに、–cached-onlyオプションを指定することで、事前にキャッシュされた外部モジュールのみが利用されることを保証できます。
deno run --lock=lock.json --cached-only --allow-net main.ts
npmパッケージへの対応は?
せっかくなので、Denoでnpmパッケージを利用した場合のバージョンのロックも同じ仕組みで行われるのかどうかを調べました。
main.tsを下記のように変更して、
import express from "npm:express";
const app = express();
app.get("/", function (req, res) {
res.send("Hello World");
});
app.listen(3000);
console.log("listening on http://localhost:3000/");
ロックファイルを生成してみたところ
deno cache --unstable --lock=lock-express.json --lock-write main.ts
{}
と依存が空になったので、npmパッケージのバージョンのロックにはまだ非対応のようです。将来的にはおそらくJSONのキーのモジュールのURLの代わりに npm:express
などと入るようになるのだと思いますが、更新を待つとしましょう。
まとめ
- Denoにはnpmのようなパッケージマネージャは存在せず、importでURLを指定することで外部モジュールを読み込む
- モジュール一元管理にはdeps.tsによるプロキシが推奨されている
- deno cacheコマンドでロックファイルを生成し、依存モジュールのバージョンを固定することも可能