Denoのモジュール管理について考える
hr.icon
Denoのモジュール管理について考えているkeroxp.icon2019/9/15 ※ Denoでは他の言語で「ライブラリ」「パッケージ」「クレート」...など呼ばれている外部ソースコードのことを一括して「モジュール」と呼ぶようにしている
Denoのリモートモジュールは、URLでしかimportできない
Node.jsのようなnpmというパッケージマネージャpackage.jsonに使うモジュールを記述する仕組みはない とはいえpackage.jsonもGemfileもGopkg.tomlもやっぱり便利だし…
serverstという僕が作っているHTTPサーバーモジュールも、deno_stdに依存していてバージョンアップのたびにソースコード中のこういうimportを一括replaceみたいなのをしていたのだけど、正直めんどかった code:ts
...
demではGoのdepに影響を受けながらも、Deno(ESM)的モジュール管理の仕組みを提示していた demのアイデアは、こうだ
code:ts
export * from "..."
export {some} from "..."
こういうやつ
"..."の部分のexportsを全部このモジュールでexportする的なやつ
これを使うと複数のモジュールのexportsを一括でまとめたり部分的に削ったりができる
https://gyazo.com/310e31f0b341efec414f850620044813
demはなんやかんやして、最終的にvendorディレクトリにこういうファイルを作る
ディレクトリの構造がDenoのキャッシュ管理とおなじになっているのがわかる
code:ts
普通ならこう書くのだが、こういう風に書くことができる
code:ts
import * 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に貼ったエイリアスにもバージョン番号が含まれているのがちょっと面倒というか、問題を解決していない印象も持った
というわけで僕はこのアイデアを少し変えてこんな仕組みを考えた
こんなの
code:modules.ts
const modules = {
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です
さらにその後に続くパス(モジュールファイルたち)はこれらです
で、こんな感じのコードを書く
code:modules.ts
const modules = {
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();
これを実行するとこんな感じのモジュールエイリアスが作られる
code:txt
vendor/
https/
deno.land/
std/
testing/
asserts.ts
mod.ts
io/
bufio.ts
....
使うときはこう
code:ts
import * as bufio from "./vendor/https/deno.land/std/io/bufio.ts"
demとの違いは、モジュール識別子にバージョン情報を入れないこと
より正確にれると、このモジュール識別子を自分で決められるということ
こうすると、deno_stdをバージョンアップしたいときはmodules.tsのここを変えるだけでいい
code:ts
version: "@v0.17.0",
そうするとプロジェクトで使っているdeno_stdのコードが全部アップデートされるので管理が楽
どうだろうか? 僕はこういう仕組みのほうが現実的に使いやすいような気もするkeroxp.icon