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)

参考資料

Deno でも Visual Studio Code の支援を受けられるようにする

最近 Deno を触り始めた。

エディタには Visual Studio Code を使っているのだが、現状では、通常の TypeScript での開発のような「ゼロコンフィグで様々な支援を受けることができる」という状態にはなっていない。
Deno での構文がエラーになるし、インテリセンスも効かない。
これでは不便だし、TypeScript を使うメリットが半減してしまうので、対策を調べた。

Visual Studio Code (以下、VSCode)のバージョンは1.46.0、Deno のバージョンは1.1.0で、動作確認している。

まずサンプルとして、以下の内容のserver.tsを用意する。
そして$ deno run --allow-net server.tsを実行すると、サーバが立ち上がる。また、Deno のバージョン情報がターミナルに表示される。

import { listenAndServe } from "https://deno.land/std/http/server.ts";

listenAndServe({ port: 8080 }, (req) => {
  if (req.method === "GET") {
    switch (req.url) {
      case "/":
        req.respond({
          status: 200,
          headers: new Headers({
            "content-type": "text/plain",
          }),
          body: "Hello Deno.",
        });
        break;
      default:
        req.respond({
          status: 404,
          headers: new Headers({
            "content-type": "text/plain",
          }),
          body: "Not found",
        });
        break;
    }
  }
});

console.log("Server running on localhost:8080");

console.log(Deno.version);

このコードの内容については、本記事の主題ではないので詳しくは触れない。
問題は、1 行目のコード。

import { listenAndServe } from "https://deno.land/std/http/server.ts";

パスとして URL を指定しており、これが Deno の特徴のひとつなのだが、通常の TypeScript ではこのような書き方はしないため、VSCode がエラーを出してしまっている。

f:id:numb_86:20200619142543p:plain

そして、末尾のconsole.log(Deno.version);でもエラーが出ている。

f:id:numb_86:20200619142556p:plain

TypeScript にDenoというオブジェクトは存在しないので、当然ではある。
だが Deno で実行する場合はDenoは存在するので、Deno のプロジェクトにおいてはこのエラーは出て欲しくない。
何より、インテリセンスが機能しないが厳しい。

f:id:numb_86:20200619142640g:plain

これらのエラーを消すために、まず、以下の拡張機能をインストールして有効にする。

marketplace.visualstudio.com

次に、$ yarn add -D typescript-deno-plugin typescriptで必要なパッケージをインストールする。

これで、ファイルを開き直すとエラーが消えている。

f:id:numb_86:20200619142616p:plain

インテリセンスも機能している。

f:id:numb_86:20200619142659g:plain

node_modulesさえあれば問題ないようなので、package.jsonyarn.lockは削除してしまう。

ちなみに、node_modulesに入っているtypescriptと Deno が使う TypeScript は別物であり、Deno を実行した時はあくまでも Deno が搭載している TypeScript を使用する。
そのため、$ yarn addで入れた TypeScript のバージョンによって実行結果が変わってしまう、ということはない。

これで、Deno でも VSCode の支援を受けながら開発できるようになった。

Deno で使う訳ではない npm パッケージ(typescript-deno-plugintypescript)をインストールしなければならないのは不格好で不便だが、Deno はまだ新しい言語であり、仕方ないのかもしれない。