30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Deno のモジュール管理やロックファイルについて

Deno では外部モジュールを利用する際、URL を指定してインポートする。
Node.js とは異なり、npm やpackage.jsonも使用しない。
ではどのようにモジュールを管理していけばよいのか。公式ドキュメントに書かれてある内容をまとめ、整理した。

Deno のv1.1.1で動作確認している。

モジュールのダウンロードとキャッシュ

まず、Deno では外部モジュールをどのように取得するのかを見ていく。

ちなみに Deno では、「パッケージ」や「ライブラリ」という言葉は使わず、「モジュール」という用語で統一する。
これは公式ドキュメントのスタイルガイドに明記されており、そうすることで混乱を避けることを意図している。

まずサンプルとして、src/sample.tsというファイルを以下の内容で作成する。

// src/sample.ts
import { createHash } from "https://deno.land/std@0.58.0/hash/mod.ts";

function md5(value: string) {
  const hash = createHash("md5");
  hash.update(value);
  return hash.toString();
}

console.log(md5("a"));

md5という、渡された文字列の MD5 ハッシュ値を返す関数を定義している。

このファイルでは、https://deno.land/std@0.58.0/hash/mod.tsという標準モジュールを利用しているが、ファイルの実行に先立ってモジュールを取得しておく必要はない。
$ deno run src/sample.tsでファイルを実行できるが、その際に自動的にダウンロードが実行される。

$ deno run src/sample.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/src/sample.ts
0cc175b9c0f1b6a831c399e269772661

モジュールのダウンロード、コンパイル、ファイルの実行、という順序で処理が行われている。

まずhttps://deno.land/std@0.58.0/hash/mod.tsをダウンロードする。
すると、そのモジュールのなかで、また別のモジュールがインポートされている。

https://deno.land/std@0.58.0/hash/mod.ts#L3-L4

そのためそのモジュールもダウンロードする。この処理を繰り返し、必要なモジュールを全てダウンロードした上で、コンパイルが行われる。

そして、一度ダウンロードしたモジュールはローカルにキャッシュされるため、ダウンロードが繰り返し行われることはない。
再度src/sample.tsを実行するとそれを確認できる。

$ deno run src/sample.ts
0cc175b9c0f1b6a831c399e269772661

ダウンロードもコンパイルも行われない。

src/sample.tsに一行書き加えて、実行してみる。

@@ -7,3 +7,4 @@
 }

 console.log(md5("a"));
+console.log(md5("b"));
$ deno run src/sample.ts
Compile file:///Users/numb/deno_deps/src/sample.ts
0cc175b9c0f1b6a831c399e269772661
92eb5ffee6ae2fec3ad71c777531578f

ファイルの内容が書き換わったのでコンパイルを実行しているが、必要なモジュールは既にキャッシュされているので、ダウンロードは行われない。

もし再度ダウンロードを行いたい場合は、--reloadフラグをつけて実行すればよい。
そうすると、キャッシュの有無に関係なく、ダウンロードやコンパイルを行ってくれる。

$ deno run --reload src/sample.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/src/sample.ts
0cc175b9c0f1b6a831c399e269772661
92eb5ffee6ae2fec3ad71c777531578f

続いて、src/sample.tsのテストを書いてみる。
まず、src/sample.tsを書き換え、md5をエクスポートする。

@@ -1,10 +1,7 @@
 import { createHash } from "https://deno.land/std@0.58.0/hash/mod.ts";

-function md5(value: string) {
+export function md5(value: string) {
   const hash = createHash("md5");
   hash.update(value);
   return hash.toString();
 }
-
-console.log(md5("a"));
-console.log(md5("b"));

次に、src/sample_test.tsを作成する。

// src/sample_test.ts
import { assertEquals } from "https://deno.land/std@0.58.0/testing/asserts.ts";

import { md5 } from "./sample.ts";

Deno.test("md5", () => {
  assertEquals(md5("a"), "0cc175b9c0f1b6a831c399e269772661");
  assertEquals(md5("b"), "92eb5ffee6ae2fec3ad71c777531578f");
});

$ deno test src/sample_test.tsでテストを実行する。
そうするとrunのときと同じように、ダウンロードとコンパイルを行ってから、テストを実行する。

$ deno test src/sample_test.ts
Download https://deno.land/std@0.58.0/testing/asserts.ts
Download https://deno.land/std@0.58.0/fmt/colors.ts
Download https://deno.land/std@0.58.0/testing/diff.ts
Compile file:///Users/numb/deno_deps/.deno.test.ts
running 1 tests
test md5 ... ok (4ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (5ms)

二回目以降はキャッシュを利用し、再ダウンロードしたい場合は--reloadフラグを付けるのも、同じ。
src/sample_test.tssrc/sample.tsをインポートしているので、そこで使っているモジュールも再ダウンロードする。

$ deno test src/sample_test.ts
running 1 tests
test md5 ... ok (2ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2ms)

$ deno test --reload src/sample_test.ts
Download https://deno.land/std@0.58.0/testing/asserts.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/fmt/colors.ts
Download https://deno.land/std@0.58.0/testing/diff.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/.deno.test.ts
running 1 tests
test md5 ... ok (4ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (4ms)

ファイルやテストは実行せず、ダウンロードとコンパイルのみを行いたい場合はcacheを使う。
runtestと同様、ダウンロードやコンパイルは必要なときにのみ行われ、--reloadフラグをつけると再実行される。

$ deno cache src/sample.ts
$ deno cache --reload src/sample.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/src/sample.ts

ここまでの内容をまとめると、以下のようになる。

  • cacheは、依存しているモジュールのダウンロードとファイルのコンパイルを行う
    • ダウンロードしたモジュールはローカルにキャッシュされる
    • ダウンロードは、キャッシュが存在しない場合にのみ実行される
    • コンパイルは、ダウンロードを行った場合、もしくはファイルの変更があった場合に、実行される
  • runtestは、まずcacheを行った上で、ファイルやテストの実行を行う
  • runtestcacheは、--reloadフラグをつけることが可能で、そうするとダウンロードとコンパイルを必ず実行する

deps.ts を使って、外部モジュールを一括管理する

既に見たように Deno では、URL を使って外部モジュールをインポートする。
モジュールのバージョンも URL のなかに埋め込む形で指定する。https://deno.land/std@0.58.0/hash/mod.tsでいえば、@0.58.0がそれに相当する。

この仕組みだと、同じモジュールを複数のファイルで使用している際に冗長になってしまう。同じ記述を複数のファイルに書いて回ることになる。
そして、バージョンを上げる際は、該当する記述を全て書き換えないといけない。
また、自分が開発しているプロダクトがどの外部モジュールに依存しているのかも、分かりづらい。

これらの問題を解決するために公式ドキュメントで提唱されている手法が、deps.tsによるモジュールの一括管理である。
必要なモジュールをdeps.tsというファイルにインポート、再エクスポートし、他のファイルはdeps.ts経由でモジュールをインポートする。

先程のサンプルコードの場合、以下のようになる。

// src/deps.ts
export { createHash } from "https://deno.land/std@0.58.0/hash/mod.ts";
export { assertEquals } from "https://deno.land/std@0.58.0/testing/asserts.ts";
// src/sample.ts
import { createHash } from "./deps.ts";

export function md5(value: string) {
  const hash = createHash("md5");
  hash.update(value);
  return hash.toString();
}
// src/sample_test.ts
import { assertEquals } from "./deps.ts";

import { md5 } from "./sample.ts";

Deno.test("md5", () => {
  assertEquals(md5("a"), "0cc175b9c0f1b6a831c399e269772661");
  assertEquals(md5("b"), "92eb5ffee6ae2fec3ad71c777531578f");
});

外部モジュールのインポートはsrc/deps.tsのみで行っているため、ここを見れば、どのバージョンのどのモジュールに依存しているのかをすぐに確認できる。
バージョンを変えたり新しくモジュールを追加したりする場合も、src/deps.tsを編集すればよい。

ロックファイルによる整合性チェック

インポート元として指定できる URL に、制限はない。GitHub に置いてあるコードであっても、CDN サービスから配信されているコードであっても、Deno が理解できるコードであるなら問題ない。
しかし、その URL が指し示しているコンテンツの内容が常に同一とは限らない。
例えばhttps://github.com/some-user/example/mod.tsをインポートしていた場合、そのリポジトリの管理者が該当するファイルを更新して内容を変えてしまうと、インポートされる内容も変わってしまう。

この問題への対応として、Deno ではロックファイルを用意している。
ロックファイルを作ることで、ダウンロードした内容の整合性をチェックすることができるようになる。

モジュールのダウンロード時に--lock=lock.json --lock-writeフラグをつけることで、lock.jsonというファイルが作られる。これがロックファイルである。
先程のsrc/deps.tsに対してcacheを実行して、ロックファイルを作ってみる。

$ deno cache --lock=lock.json --lock-write src/deps.ts

lock.jsonの中身は、URL とハッシュ値のペア。

{
  "https://deno.land/std@0.58.0/hash/mod.ts": "efbedc6b108c88cf2f5db2fbf79bcdf2230734c6111e9f84441d857e5d5d4b84",
  "https://deno.land/std@0.58.0/testing/diff.ts": "77338e2b479626c096278d7b4a95f750753ee39558f314db6b794e8d8a98d515",
  "https://deno.land/std@0.58.0/fmt/colors.ts": "06444b6ebc3842a4b2340d804bfa81fc5452a03513cbb81bd7f6bf8f4b8f3ac4",
  "https://deno.land/std@0.58.0/testing/asserts.ts": "bbd5b5ac3c106df2a56de9dfccc53084aa522299b0f00af6987987d82abd2087",
  "https://deno.land/std@0.58.0/encoding/hex.ts": "fa2206fb59cd5098dacaa82f72d1400d763d1497be7e6d5bed3ef964e03961e1",
  "https://deno.land/std@0.58.0/hash/_wasm/hash.ts": "ef67284c0fda6899ec0fead54b16ffbe6dbe6ab6efdfaf9f4f182b4e5e77971a",
  "https://deno.land/std@0.58.0/encoding/base64.ts": "5f8476fb14abcd0c6546bd75ad5c8b189c6eab3f0a8ad2d0f15e65fa6c72708d",
  "https://deno.land/std@0.58.0/hash/_wasm/wasm.js": "32831e605a5a533c6d11adbb6e5e5f2304b409859ca57ba86db4945457281bd6"
}

その URL からダウンロードされる内容が同一である限り、ハッシュ値も変わらない。
そのため、改めてダウンロードを行った際にそのときのハッシュ値とlock.jsonの内容を比較することで、ダウンロードされた内容に変化がないかをチェックできる。
具体的には、ダウンロード時に--lock=lock.jsonをつければよい。

例えば以下のコマンドを実行すれば、整合性をチェックしつつ依存モジュールをダウンロードできる。

$ deno cache --reload --lock=lock.json src/deps.ts

もし整合性が取れていなければ、エラーを出す。
例えばlock.jsonに書かれてあるハッシュ値を書き換えてcacheを実行すると、以下のような結果になる。

$ deno cache --reload --lock=lock.json src/deps.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/testing/asserts.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/fmt/colors.ts
Download https://deno.land/std@0.58.0/testing/diff.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/src/deps.ts
Subresource integrity check failed --lock=lock.json
https://deno.land/std@0.58.0/hash/mod.ts
$ echo $?
10

終了ステータスが0でないので、コマンドが失敗したことが分かる。

lock.jsonを更新したい場合は、作成時と同じように--lock=lock.json --lock-writeフラグをつければよい。

キャッシュした内容を Git で保存する

上述したように、ロックファイルという仕組みによって、URL からダウンロードされる内容の整合性をチェックできる。
これによって、ダウンロードされる内容が変わってしまったときに、それに気付けるようになった。

しかし、気付けたとしても、その際の対応策を考えておかなければ意味がない。
そして、整合性どころか、そもそもインポートしたい URL にアクセスできなくなる可能性がある。先程の GitHub のリポジトリの例で言えば、管理者がリポジトリを削除したり非公開にしたりする可能性がある。あるいは、GitHub そのものに障害が発生して、一時的にアクセス不能になるかもしれない。
これは特に、本番環境において深刻な問題となる。

様々な考え方やアプローチがあると思うが、ここでは、公式ドキュメントに記載されている対応策を紹介する。

それは単純に、キャッシュしたファイルを Git で管理するというもの。
キャッシュを保存するディレクトリはDENO_DIRという環境変数で指定できるので、それを利用する。

$ DENO_DIR=./deno_dir deno cache src/deps.tsを実行すると、src/deps.tsが依存しているモジュールをダウンロードし、./deno_dirにキャッシュとして保存する。

$ DENO_DIR=./deno_dir deno cache src/deps.ts
Download https://deno.land/std@0.58.0/hash/mod.ts
Download https://deno.land/std@0.58.0/testing/asserts.ts
Download https://deno.land/std@0.58.0/hash/_wasm/hash.ts
Download https://deno.land/std@0.58.0/hash/hasher.ts
Download https://deno.land/std@0.58.0/fmt/colors.ts
Download https://deno.land/std@0.58.0/testing/diff.ts
Download https://deno.land/std@0.58.0/hash/_wasm/wasm.js
Download https://deno.land/std@0.58.0/encoding/hex.ts
Download https://deno.land/std@0.58.0/encoding/base64.ts
Compile file:///Users/numb/deno_deps/src/deps.ts

それ以降、runtestを実行する際にDENO_DIRに対して./deno_dirを指定すれば、そこに保存されているキャッシュを利用するようになる。
つまり、URL 先がアクセス不能になったとしても、確実にキャッシュ時と同じ内容のモジュールを利用することができる。

$ DENO_DIR=./deno_dir deno test src/sample_test.ts
Compile file:///Users/numb/deno_deps/.deno.test.ts
running 1 tests
test md5 ... ok (5ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (5ms)

参考資料