Denoのモジュール管理について考える
Denoのモジュール管理について考えている

2019/9/15
※ Denoでは他の言語で「ライブラリ」「パッケージ」「クレート」...など呼ばれている外部ソースコードのことを一括して「モジュール」と呼ぶようにしている
Denoのリモートモジュールは、URLでしかimportできない
Node.jsのような
npmというパッケージマネージャ
package.json
に使うモジュールを記述する仕組みはない
とはいえ package.json
も Gemfile
も Gopkg.toml
もやっぱり便利だし…
serverstという僕が作っている
HTTPサーバーモジュールも、
deno_stdに依存していてバージョンアップのたびにソースコード中のこういうimportを一括replaceみたいなのをしていたのだけど、正直めんどかった
tsimport * as path from "https://deno.land/std@v0.17.0/fs/path.ts
...
demでは
Goの
depに影響を受けながらも、Deno(ESM)的モジュール管理の仕組みを提示していた
demのアイデアは、こうだ
tsexport * from "..."
export {some} from "..."
こういうやつ
"..."
の部分のexportsを全部このモジュールでexportする的なやつ
これを使うと複数のモジュールのexportsを一括でまとめたり部分的に削ったりができる
demはなんやかんやして、最終的に vendor
ディレクトリにこういうファイルを作る
ディレクトリの構造がDenoのキャッシュ管理とおなじになっているのがわかる
tsimport * as server from "https://deno.land/std@v0.16.0/http/server.ts"
普通ならこう書くのだが、こういう風に書くことができる
tsimport * as server from "./vendor/https/deno.land/std@v0.16.0/http/server.ts"
これはESMのモジュールエイリアスとでも呼ぶべきものだ
この2つの記述はDeno(というかESM的)には等価になる(最終的なexportsが同じなので)
このメリットとしては、リモートモジュールのモジュール識別子( https://...
の部分)をソースコードから消せることだ
URL形式のモジュール識別子は、URLが変わったりバージョンを変えたりすると頻繁に修正しなくてはいけない
これはかなり骨の折れる作業で、僕も結構最初くらいからきっつと思っていた
特に、URLにバージョンが入った形式だとプロジェクトのすべてのimportをreplaceしないといけなくて大変だ
demはvendorに貼ったエイリアスにもバージョン番号が含まれているのがちょっと面倒というか、問題を解決していない印象も持った
というわけで僕はこのアイデアを少し変えてこんな仕組みを考えた
こんなの
modules.tsconst modules = {
"https://deno.land/std": {
version: "@v0.17.0",
modules: [
"/testing/mod.ts",
"/testing/asserts.ts",
"/textproto/mod.ts",
"/io/bufio.ts",
"/io/readers.ts",
"/io/writers.ts",
"/strings/decode.ts",
"/strings/encode.ts"
]
}
};
まずこんなかんじのモジュールリスト的なやつを作る
これはdemの dem.json
に少し似ているがより簡素化されている。あとJSONじゃない
これはこういうことを記述している
https://deno.land/std
以下にあるURLモジュールを使います
/std
の後に続くpostfix(バージョン)は @v0.17.0
です
さらにその後に続くパス(モジュールファイルたち)はこれらです
で、こんな感じのコードを書く
modules.ts#!/usr/bin/env deno --allow-write
import * as path from "https://deno.land/std@v0.17.0/fs/path.ts";
const modules = {
"https://deno.land/std": {
version: "@v0.17.0",
modules: [
"/testing/mod.ts",
"/testing/asserts.ts",
"/textproto/mod.ts",
"/io/bufio.ts",
"/io/readers.ts",
"/io/writers.ts",
"/strings/decode.ts",
"/strings/encode.ts"
]
}
};
async function ensure() {
const encoder = new TextEncoder();
for (const [k, v] of Object.entries(modules)) {
const url = new URL(k);
const { protocol, hostname, pathname } = url;
const scheme = protocol.slice(0, protocol.length - 1);
const dir = path.join("./vendor", scheme, hostname, pathname);
const writeLinkFile = async (mod: string) => {
const modFile = `${dir}${mod}`;
const modDir = path.dirname(modFile);
await Deno.mkdir(modDir, true);
const specifier = `${k}${v.version}${mod}`;
const link = `export * from "${specifier}";`;
const f = await Deno.open(modFile, "w");
await Deno.write(f.rid, encoder.encode(link));
console.log(`Linked: ${specifier}`);
};
await Promise.all(v.modules.map(writeLinkFile));
}
}
ensure();
これを実行するとこんな感じのモジュールエイリアスが作られる
txtvendor/
https/
deno.land/
std/
testing/
asserts.ts
mod.ts
io/
bufio.ts
....
使うときはこう
tsimport * as bufio from "./vendor/https/deno.land/std/io/bufio.ts"
demとの違いは、モジュール識別子にバージョン情報を入れないこと
より正確にれると、このモジュール識別子を自分で決められるということ
こうすると、deno_stdをバージョンアップしたいときは modules.ts
のここを変えるだけでいい
そうするとプロジェクトで使っているdeno_stdのコードが全部アップデートされるので管理が楽
どうだろうか? 僕はこういう仕組みのほうが現実的に使いやすいような気もする
