ECMAScript Module Harmony
変更情報
【2024/08/20】
- WasmCG で進行している ES Module Integration についての記述を追加
-
Import Attributes の
assert
キーワードが正式に仕様から取り除かれたのに追随
【2024/05/26】
- せっかくなので好きなTC39 Proposals発表ドラゴンをファーストビューに追加
- モジュールインスタンスフェーズがなくなり、ソースフェーズと同じレイヤーへと変更されたのに追随
-
Import Attributes で
assert
キーワードの廃止が検討されていることを追記 - 同期モジュールアサートがディレクティブを使うよう変更されたのに追随
- Compartments のインターフェースが変更されたのに追随し、独立の章としてまとめるよう変更
好きなTC39 Proposals発表ドラゴン(ニコニコ動画、YouTube)
Module Harmony とは
現在 TC39 で多くのモジュール機能を追加する提案が進行しています。かつてはそれぞれの提案が無秩序に進行していましたが、2023年5月に Module Harmony として整理されました。この記事ではそれら提案についてまとめようと思います。
モジュール提案仕様の依存関係
インポートフェーズ修飾子
モジュールの読み込みがフェーズに分けられ、それぞれに対して修飾子が提案されています。シンタックスとしては import
の後ろに修飾子を付けます。
import <Modifier> <ImportBinding> from <ModuleSpecifier>;
import.<Modifier>(<AssignmentExpression>);
モジュールの各フェーズと修飾子の対応
アセット参照
URL やパス解決のみをするのがアセット参照です。
AssetReference
インスタンスを生成し、Dynamic Imports で改めて読み込むことができます。
import asset fooRef from "./foo.mjs";
const { foo } = await import(fooRef);
モチベーションはいくつかあります。
- 他のモジュールに
AssetReference
インスタンスを渡し、モジュールの相対パスを組み立て直す必要なく Dynamic Imports で読み込むタイミングを制御できる - モジュールの依存に含めることができ、画像や CSS などの存在をバンドラーに教えることができる
- Deno で事前にキャッシュさせて特別な権限許可設定が必要なく実行できる
import asset Logo from "./logo.gif";
function loadLogo() {
const img = new Image();
img.src = URL.createObjectURL(Logo);
return img;
}
ソースフェーズインポート
フェッチ、単一ファイルのコンパイルまで行うのがソースフェーズインポートです。
WebAssembly
WebAssembly.Module
を作ることができます。
import source fooSource from "./foo.wasm";
console.log(fooSource instanceof WebAssembly.Module); // => true
const fooInstance = await WebAssembly.instantiate(fooSource, {/* imports */});
const { foo } = fooInstance.exports;
モチベーションはいくつかあります。
-
WebAssembly.instantiateStreaming
を使わないですみ、フロントエンドとサーバーサイド JavaScript で同一の記述ができる - モジュールの依存に含めることができ、バンドル時に最適化ができるようになる
- ブラウザ環境において Content Security Policy の
unsafe-eval
(wasm-unsafe-eval
) が不要になる
なお、これは今まで通り WebAssembly がインポートする依存函数を JavaScript から実行時に指定する方式です。ソースフェーズインポートとは別に WebAssembly 内部で直接依存モジュールを記述する提案については WasmCG で進行しています。
import { foo } from "./foo.wasm";
ECMAScript (JavaScript)
ModuleSource
インスタンスを生成し、Dynamic Imports に加えて Web Worker でも扱うことができます。
import source fooModule from "./foo.mjs";
const { foo } = await import(fooModule);
import source fooModule from "./foo.mjs";
const fooWorker = new Worker(fooModule, { type: "module" });
同一の ModuleSource
インスタンスを Dynamic Imports した場合、同一のネームスペースオブジェクトを返します。
import source fooModule from "./foo.mjs";
const fooNameSpace1 = await import(fooModule);
const fooNameSpace2 = await import(fooModule);
console.log(fooNameSpace1 === fooNameSpace2); // => true
モジュール式
インラインでモジュールを作ることができるのがモジュール式です。これにより今までインラインで Web Worker を作りたい場合、文字列から Blob
を作り URL.createObjectURL
を使うハックが使われていましたが、それが不要になります。
const fooModule = module {
self.addEventListener("message", (e) => {});
};
const fooWorker = new Worker(fooModule, { type: "module" });
モジュール宣言
単一ファイルで複数モジュールを扱えるようにするのがモジュール宣言です。この提案は直接ユーザーが記述するというよりかはバンドラーによって使われることが想定されています。
module Foo {
export function foo() {
console.log("foo");
}
}
import { foo } from Foo;
現状バンドラーは単一ファイルにまとめる際にモジュールの挙動をエミュレートしています。Top-Level Await などの新しいモジュール機能が入るとその度に実装が複雑になり、パフォーマンスコストもかかってしまいます。それを解決するための提案です。
遅延インポート評価
リンクまで行うのが遅延インポート評価です。ネームスペースインポートのみサポートしています。
最初にネームスペースオブジェクトにアクセスされるまでモジュールの実行を遅延させることで、パフォーマンス向上を図ることができます。
import defer * as fooNameSpace from "./foo.mjs";
// この函数が最初に実行されたときに ./foo.mjs の中身が実行される
function callFoo() {
return fooNameSpace.foo();
}
export defer from
文を追加するかどうかや、Top-Level Await を使った非同期モジュールに対して読み込んだ場合にどうするかは議論中です。
Import Attributes
インポート文に属性を付与できるのが Import Attributes です。この提案自体は構文を追加するだけですが、JSON Modules に使われる大元となっています。
import json from "./foo.json" with { type: "json" };
import("foo.json", { with: { type: "json" } });
属性は実行環境によってサポートするものが異なります。Web 標準においては CSS Modules や Text/Bytes Modules が提案されています。なお実行環境でサポートしていない属性を記述した場合は例外を投げることになっています。
モジュール同期アサート
モジュールの依存に Top-Level Await を使った非同期モジュールが含まれないことをアサートします。
"assert sync";
// Top-Level Await を使った非同期モジュールが含まれないかアサートする
import { foo } from "./foo.mjs";
Service Worker ではモジュール依存に非同期モジュールを含めることが出来ません。ファイルにこのディレクティブを付けることで実装エンジンへのヒントはもちろんのこと、バンドル時にチェックできます。
Compartments
Compartment
クラスによってモジュールを別のグローバルスコープに隔離し、依存モジュールをフックすることができます。
const compartment = new Compartment({
resolveHook(importSpecifier, referrerSpecifier) {
return new URL(importSpecifier, referrerSpecifier).href;
},
async loadHook(url) {
const response = await fetch(url);
const source = await response.text();
return {
source: new ModuleSource(source),
specifier: response.url,
importMeta: { url: response.url },
};
},
});
await compartment.import('https://example.com/example.js');
締め
ざっくり Module Harmony をまとめてみました。インポートフェーズ修飾子の各提案についてはモチベーションはわかるものの、結構複雑でそれぞれの使い分けが難しそうですね。
引き続き今後どの様になるのか注視していこうと思います。個人的に Scrapbox にて TC39 meeting で決まったことをまとめています。よければこちらも御覧ください。
Discussion