7月30日、Denoで「HTTPインポートに関して私たちが間違っていたこと(What we got wrong about HTTP imports)」と題した記事が公開された。この記事では、Deno、そしてNode.jsの開発者であるライアン・ダール自身が、DenoのHTTPインポートの設計に関する誤りと、それに対する解決策について(そして、JSRとDeno 2のPRも含め)述べている。
以下に、その内容を詳しく紹介する。
本記事は、以下のエキスパートに監修していただきました:
古川陽介さん(Japan Node.js Association代表理事)
HTTPインポートとは
DenoのモジュールシステムをHTTPインポートに基づいて設計するという試みは、npmに代わる分散システムをHTTP上で実現することを目指していた。これにより、package.json
ファイルやnode_modules
フォルダーが不要になり、プロジェクト構造がシンプルになる。
Denoのスクリプトは、プロジェクトディレクトリや設定なしで単一ファイルのプログラムとして実行できる。例えば、次のように標準ライブラリからassertEquals()
関数をインポートできる:
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
assertEquals(1, 2);
このアイデアは画期的だったが、HTTPインポートにはいくつかの問題が明らかになった。
(監修者注: ちなみに、Node.jsではHTTP importを行う機構が一旦実装されたものの削除されている)
HTTPインポートの課題1: URLの長さ
長いURLがコード内に大量に出現する。以下のimportのどちらがシンプルかは一目瞭然だ。
// Node.jsスタイルのインポート
import express from "express";
// DenoスタイルのHTTPインポート
import oak from "https://deno.land/x/oak@v16.1.0";
HTTPインポートの課題2: 依存関係の管理
長いURLが大規模なコードベース中に散乱することで、依存関係の管理が煩雑になる。初期には、依存関係をプロジェクト内の単一ファイルに集中させるためにdeps.ts
の慣例が採用された:
export { concat } from "https://deno.land/std@0.200.0/bytes/mod.ts";
export * as base64 from "https://deno.land/std@0.200.0/encoding/base64.ts";
そして、プロジェクトの各ファイルはdeps.tsをインポートする:
import { concat } from "../../deps.ts";
この方法は一応機能するが、Node.jsにおけるpackage.json
ファイルの単純さに比べると、面倒なのは否めない。
HTTPインポートの課題3: 依存関係のバージョニングとバージョンの重複
URLにはセマンティックバージョニングがないため、依存関係の管理が困難になる。バージョン文字列をURLに埋め込むことはできるが、手動で更新する必要があるため、複数のバージョンが混在しやすい:
// 0.200.0と0.224.0が混在!
import { concat } from "https://deno.land/std@0.200.0/bytes/mod.ts";
import { concat } from "https://deno.land/std@0.224.0/bytes/mod.ts";
セマンティックバージョニングは依存関係の重複を減らし、読み込まれるモジュールの数を減少させる。
理想的には、Denoは交換可能なモジュールを認識し、最新バージョンを使用するように動作するべきだ。
HTTPインポートの課題4: 信頼性
分散型モジュールシステムは信頼性の問題も引き起こした。
多くのモジュールが個人のサーバーにホストされているため、それらのサーバーがダウンすると、CIや新しいデプロイを停止させる原因になった。Denoはリモートの依存関係をキャッシュするため、すぐに問題になるわけではないが、信頼性の低いホストに依存することが全体の可用性を低下させた。
解決策:インポートマップとJSR
これらの問題を解決するために、DenoはインポートマップとJSRという二つの主要な改善を導入した。
インポートマップとは
インポートマップとは、JavaScriptのモジュールシステムにおいて、モジュールのインポートパスを短縮し、管理しやすくするための仕組みである。これは、ウェブ標準の一部として提案されており、ブラウザとJavaScriptランタイム(例えばDeno)でサポートされている。
インポートマップを使用することで、長いURLや複雑なディレクトリ構造を単純化し、短く覚えやすいインポートパスに置き換えることができる。これにより、コードの可読性が向上し、依存関係の管理が容易になる。
インポートマップは、通常、JSON形式で記述され、インポートパスと実際のモジュールURLのマッピングを定義する。以下は基本的な例だ:
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/lodash-es/lodash.js",
"axios": "https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"
}
}
上記の例では、lodash
とaxios
という短いインポートパスが定義され、それぞれ実際のURLにマッピングされている。これにより、次のようにインポートできる:
import _ from 'lodash';
import axios from 'axios';
以下のように、インポートはいくつでも指定できる。
{
"imports": {
"$ga4": "https://raw.githubusercontent.com/denoland/ga4/main/mod.ts",
"$marked-mangle": "https://esm.sh/marked-mangle@1.0.1",
"@astral/astral": "jsr:@astral/astral@^0.4.0",
"@fresh/plugin-tailwind": "./plugin-tailwindcss/src/mod.ts",
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.10.3"
}
}
Denoでこのインポートマップを使用するには、スクリプトを実行する際にインポートマップファイルを指定する:
deno run --import-map=import_map.json script.ts
しかしインポートマップだけでは、セマンティックバージョニングの問題や信頼性の問題を解決できない。そこで、JSRが登場する。
JSRとは
JSRはオープンソースのクロスランタイムコードレジストリだ。
JavaScriptだけでなく、TypeScriptでのパッケージ公開に対応しているだけでなく、セマンティックバージョニングを理解し、常に互換性のある最新バージョンをインポートすることができる。JSRはHTTPインポートの良い部分を継承しており、実際にインポートされるコードだけをダウンロードすることで、(Node.jsのように)大きなtarballをダウンロードする必要がない。
// 従来のHTTPインポート
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
// JSRを用いたインポート
import { assertEquals } from "jsr:@std/assert@1";
assertEquals(1, 2);
インポートマップを追加することで、さらに短く依存関係を指定することができる。
import { assertEquals } from "@std/assert";
assertEquals(1, 2);
{
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
HTTPインポートを用いた既存のDenoスクリプトも引き続き動作するが、deps.ts
の代わりにインポートマップ、deno.land/x
やnpmの代わりにJSRが推奨されている。
Deno 2が間もなく登場
Deno 2では、モジュールの共有にJSR、バージョニングにセマンティックバージョニング、依存関係管理にインポートマップが導入される。さらに、ワークスペースとモノレポのサポート、Node/npm互換性の向上などが含まれる。
詳細はWhat we got wrong about HTTP importsを参照していただきたい。