🦕

Deno v1.19で追加されたdeno vendorコマンドについて

2022/02/28に公開

はじめに

2022年2月17日にDeno v1.19がリリースされました。

https://github.com/denoland/deno/releases/tag/v1.19.0

このリリースではdeno vendorという新しいコマンドが追加されています。

この記事ではこのdeno vendorコマンドの使い方などについて解説します。

deno vendorコマンドとは?

下記の問題を解消するべく追加されたコマンドです。

  • サードパーティモジュールをバージョン管理に含めるのが難しい
  • サードパーティモジュールのデバッグが難しい

従来では、サードパーティモジュールをバージョン管理に含めるためには、DENO_DIRという環境変数を操作する必要がありました。

$ DENO_DIR=.deno deno run mod.ts

これにより、サードパーティモジュールは.denoに保存されるようになるため、このディレクトリをバージョン管理に追加すれば、目的を達成できます。

しかし、この方法には下記のような問題があります。

  • ダウンロードされたサードパーティモジュールはファイル名がハッシュ化された状態で保存されるため、特定のファイルを探したりデバッグするのが困難
  • DENO_DIRにはダウンロードされたサードパーティモジュールだけでなく、トランスパイル後のJavaScriptファイルも保存されるため、容量が大きくなってしまう。
    • また、DenoのバージョンアップによりDENO_DIRの構造が変わると、互換性を維持できなくなるという問題もあります。

これらの問題を解消するために、deno vendorコマンドが追加されました。

これから、具体的にdeno vendorの使い方を説明していきます。

使い方

使い方はdeno rundeno cacheコマンドなどに似ていて、スクリプトのエントリポイントとなるファイルをdeno vendorの引数として指定します。

$ deno vendor mod.ts
Vendored 70 modules into vendor/ directory.

To use vendored modules, specify the `--import-map` flag when invoking deno subcommands:
  deno run -A --import-map vendor/import_map.json mod.ts

これにより、Denoはmod.ts及びそこから再帰的にimportされているモジュールを分析し、依存されている全てのサードパーティモジュールをvendorディレクトリへ保存します。

vendor
├── deno.land
│   ├── std@0.127.0
│   │   ├── _util
│   │   │   ├── assert.ts
│   │   │   └── os.ts
│   │   ├── async
│   │   │   └── deferred.ts
│   │   ├── bytes
│   │   │   ├── bytes_list.ts
│   │   │   ├── equals.ts
│   │   │   └── mod.ts
│   │   ├── encoding
│   │   │   └── base64.ts
│   │   ├── fmt
│   │   │   └── colors.ts
│   │   ├── fs
│   │   │   └── exists.ts
│   │   ├── io
│   │   │   ├── buffer.ts
│   │   │   ├── bufio.ts
│   │   │   ├── files.ts
│   │   │   ├── types.d.ts
│   │   │   └── util.ts
│   │   ├── path
│   │   │   ├── _constants.ts
│   │   │   ├── _interface.ts
│   │   │   ├── _util.ts
│   │   │   ├── common.ts
│   │   │   ├── glob.ts
│   │   │   ├── mod.ts
│   │   │   ├── posix.ts
│   │   │   ├── separator.ts
│   │   │   └── win32.ts
│   │   ├── streams
│   │   │   └── conversion.ts
│   │   └── testing
│   │       ├── _diff.ts
│   │       └── asserts.ts
│   └── std@0.84.0
│       └── encoding
│           └── utf8.ts
├── import_map.json
... 省略 ...

このvendorディレクトリをGitなどのバージョン管理システムに追加することで、サードパーティモジュールのバージョン管理が実現できます。

また、もう一つ重要な点としてvendor/import_map.jsonというファイルが生成されています。

vendor/import_map.json
{
  "imports": {
    "https://deno.land/": "./deno.land/",
    "https://unpkg.com/": "./unpkg.com/"
  },
  "scopes": {
    "./unpkg.com/": {
      "./unpkg.com/puppeteer@13.3.2/lib/esm/puppeteer/common/helper.js": "./unpkg.com/puppeteer@13.3.2/lib/esm/puppeteer/common/helper.js"
    }
  }
}

--import-mapオプションでこのファイルを指定することで、Denoがvendorディレクトリに保存されたサードパーティモジュールを読み込んでくれるようになります。

# スクリプトの実行
$ deno run --import-map=vendor/import_map.json mod.ts

# テストの実行
$ deno test --import-map=vendor/import_map.json test.ts

サードパーティモジュールのデバッグについて

deno vendorDENO_DIRを変更するアプローチと異なり、ダウンロードしたサードパーティモジュールのファイル名は基本的にそのまま維持してくれます。

それにより、サードパーティモジュールのデバッグが容易になるというメリットがあります。
そのため、deno vendorコマンドはサードパーティモジュールに関わるデバッグ用途でも便利です。

まず、deno vendorvendorディレクトリを生成しましょう。

$ deno vendor mod.ts

その後、vendorディレクトリに保存されたファイルにconsole.logを仕込んでみましょう。

$ vi vendor/deno.land/std@0.127.0/fs/exists.ts
# console.log("foobar")を追加

$ deno run --import-map=vendor/import_map.json mod.ts
...
foobar

デバッグが済んでvendorディレクトリが不要になったら、削除するだけで後片付けができます。

$ rm -rf ./vendor

より詳細にデバッグを行いたいときは、debugger文vendorディレクトリ内のコードに仕込むとよいでしょう。

debugger;

debugger文を仕込んだ後、下記コマンドを実行することで、Chrome DevToolsを使用してデバッグができます。

$ deno run --import-map=vendor/import_map.json --inspect-brk mod.ts

Denoでのデバッグに関しては、下記も参照ください。

https://deno.land/manual@v1.19.0/getting_started/debugging_your_code

サードパーティモジュールのアップデートについて

例えば、deno_stdのバージョンをv0.126.0からv0.127.0へアップデートしたいとします。

ソースコード中のimport文を変更後、deno vendorを実行すると、次のようなエラーが発生してしまいます。

$ deno vendor mod.ts
error: Output directory was not empty. Please specify an empty directory or use --force to ignore this error and potentially overwriteits contents.

この問題は次のように--forceオプションを指定すれば解決します。

$ deno vendor --force mod.ts

しかし、このやり方だと、古いバージョンのdeno_std(v0.126.0)が残ったままになってしまいます。

$ tree vendor/deno.land
vendor/deno.land
├── std@0.126.0
│   ├── _util
│   │   ├── assert.ts
│   │   └── os.ts
│   ... 省略 ...
├── std@0.127.0
│   ├── _util
│   │   ├── assert.ts
│   │   └── os.ts
   ... 省略 ...

この場合は、手動で古いバージョンを削除すれば解決します。

$ rm -rf ./vendor/deno.land/std@0.126.0
# (補足) すでに`vendor`がバージョン管理されている場合は`rm`ではなく`git rm`を使いましょう

ただし、このやり方だと古いバージョンのモジュールを消し忘れる可能性があります。

可能であれば、vendorディレクトリを削除してから再度deno vendorコマンドを実行すると安全でしょう。

$ rm -rf ./vendor
$ deno vendor mod.ts

vendorディレクトリをdeno lintdeno fmtの対象から外す

deno vendorでダウンロードされたサードパーティモジュールは./vendorディレクトリに保存されるため、そのままではdeno lintdeno fmtの適用対象として扱われてしまいます。

これを避けるためには、deno.jsonというファイルを用意するとよいです。

Denoはdeno.jsondeno.jsoncという名前の設定ファイルを用意しておくことで挙動をカスタマイズすることができます。(Deno v1.18以降のバージョンであれば、deno.json(c)は自動で読み込まれます)

https://qiita.com/access3151fq/items/7aa44ee69b4a5ff867c7

下記のような内容でdeno.jsonを作成してみましょう。

deno.json
{
  "lint": {
    "files": {
      "exclude": ["vendor/"]
    }
  },
  "fmt": {
    "files": {
      "exclude": ["vendor/"]
    }
  }
}

これにより、vendorディレクトリのサードパーティモジュールをdeno lintdeno fmtの対象外とすることができます。

--import-mapの指定が面倒なときは

VelociraptorやMakeなどのツールを使う

deno vendorで保存されたサードパーティモジュールを利用するには、毎回--import-mapオプションを指定する必要があります。

これが面倒な場合、VelociraptorやMakeなどのツールを使用することで負担を軽減できます。

例えば、Velociraptorであれば、下記のような設定ファイルを用意しておくことで--import-mapオプションの指定の手間を省けます。

scripts.yaml
scripts:
  start:
    cmd: mod.ts
    importMap: vendor/import_map.json

deno.json(c)でのサポートについて

現在、deno.json(c)ファイルでImport mapファイルの指定をサポートするPRが作成され、すでにマージされています。

https://github.com/denoland/deno/pull/13739

この機能がDeno本体に入れば、Velociraptorなどのツールに頼らなくても--import-mapオプションを指定する手間が省けます。

deno.json
{
  "importMap": "./vendor/import_map.json"
}

この機能は、次のマイナーアップデートであるDeno v1.20で入る可能性が高そうなので、deno vendorを利用する際などに活用できそうです。

おわりに

Deno v1.19では、deno vendorコマンド以外にも様々な変更が行われています。

  • --promptオプションの挙動がデフォルト化
  • CompressionStreamDecompressionStreamのサポート
  • Deno.FileDeno.FsFileへのリネーム
  • シグナルリスナAPIの安定化

これらの詳細については、公式のリリースポストで触れられているため、興味のある方はそちらも参照してみてください!

https://deno.com/blog/v1.19

参考

Discussion