🎶

ECMAScript Module Harmony

2024/01/04に公開

Module Harmony とは

現在 TC39 で多くのモジュール機能を追加する提案が進行しています。かつてはそれぞれの提案が無秩序に進行していましたが、2023年5月に Module Harmony として整理されました。この記事ではそれら提案についてまとめようと思います。

https://docs.google.com/presentation/d/1mZrAHHimtM_z_8fM9L3DUXFz5bjlJPxx8VrwsC68hmk/edit#slide=id.p


モジュール提案仕様の依存関係

インポートフェーズ修飾子

モジュールの読み込みがフェーズに分けられ、それぞれに対して修飾子が提案されています。シンタックスとしては import の後ろに修飾子を付けます。

import <Modifier> <ImportBinding> from <ModuleSpecifier>;
import.<Modifier>(<AssignmentExpression>);


モジュールの各フェーズと修飾子の対応

アセット参照

URL やパス解決のみをするのがアセット参照です。AssetReference オブジェクトを生成し、Dynamic Imports で改めて読み込むことができます。

import asset fooRef from "./foo.mjs";

const { foo } = await import(fooRef);

https://github.com/tc39/proposal-asset-references

モチベーションはいくつかあります。

  • 他のモジュールに AssetReference オブジェクトを渡し Dynamic Imports で読み込むタイミングを制御できる
  • モジュールの依存に含めることができ、画像や CSS などの存在をバンドラーに教えることができる
  • Deno で事前にキャッシュさせて特別な権限許可設定なく実行できる
import asset Logo from "./logo.gif";

function loadLogo() {
  const img = new Image();
  img.src = URL.createObjectURL(Logo);
  return img;
}

"Source" フェーズインポート

フェッチ、コンパイルまで行うのが "Source" フェーズインポートです。現状 WebAssebly のための提案と言うことができ、WebAssembly.Module を作ることができます。

import source fooSource from "./foo.wasm";

console.log(fooSource instanceof WebAssembly.Module); // => true
const fooInstance = await WebAssembly.instantiate(fooSource, {/* imports */});

https://github.com/tc39/proposal-source-phase-imports

これは従来の Fetch API と WebAssembly.instantiateStreaming を使う場合に比べて、モジュールの依存に含められるのはもちろんのこと、ブラウザ環境において Content Security Policy の unsafe-eval (wasm-unsafe-eval) が不要になるベネフィットがあります。

将来的には Compartments の提案によって JavaScript に対しても "Source" フェーズインポートすることができるようになり、取得した ModuleSource オブジェクトから次項の Module インスタンスが作れるようになります。

import source fooSource from "./foo.mjs";

const fooModule = new Module(fooSource, {
  async importHook(specifier, attributes) {
    return /* Module instance */;
  },
});

"Module instance" フェーズインポート

コンテキストのアタッチまで行うのが "Module instance" フェーズインポートです。Dynamic Imports に加えて Web Worker に Module インスタンスを渡すことができます。

import module fooModule from "./foo.mjs";

const { foo } = await import(fooModule);
import module fooModule from "./foo.mjs";

const fooWorker = new Worker(fooModule, { type: "module" });

https://github.com/lucacasonato/proposal-module-instance-imports

同一の Module インスタンスを Dynamic Imports した場合、同一のネームスペースオブジェクトを返します。

import module fooModule from "./foo.mjs";

const fooNameSpace1 = await import(fooModule);
const fooNameSpace2 = await import(fooModule);
console.log(fooNameSpace1 === fooNameSpace2); // => true

モジュール式

インラインで Module インスタンスを作ることができるのがモジュール式です。これにより今までインラインで Web Worker を作りたい場合、文字列から Blob を作り URL.createObjectURL を使うハックが使われていましたが、それが不要になります。

const fooModule = module {
  self.addEventListener("message", (e) => {});
};

const fooWorker = new Worker(fooModule, { type: "module" });

https://github.com/tc39/proposal-module-expressions

遅延インポート評価

リンクまで行うのが遅延インポート評価です。ネームスペースインポートのみサポートしています。最初にアクセスされるまで実行を遅延させることで、最初の実行のパフォーマンス向上を図ることができます。

import defer * as fooNameSpace from "./foo.mjs";

// この函数が最初に実行されたときに ./foo.mjs の中身が実行される
function callFoo() {
  return fooNameSpace.foo();
}

https://github.com/tc39/proposal-defer-import-eval

export defer を追加するかどうかや、Top-Level Await を使った非同期モジュールに対して読み込んだ場合にどうするかは議論中です。

モジュール宣言

単一ファイルで複数モジュールを扱えるようにするのがモジュール宣言です。この提案は直接ユーザーが記述するというよりかはバンドラーによって使われることが想定されています。

module Foo {
  export function foo() {
    console.log("foo");
  }
}

import { foo } from Foo;

https://github.com/tc39/proposal-module-declarations

現状バンドラーは単一ファイルにまとめる際にモジュールの挙動をエミュレートしています。Top-Level Await などの新しいモジュール機能が入るとその度に実装が複雑になり、パフォーマンスコストもかかってしまいます。それを解決するための提案です。

Import Attributes (旧 Import Assertions)

インポート文に属性を付与できるのが Import Attributes です。この提案自体は構文を追加するだけですが、JSON ModulesCSS Modules などに使われる大元となっています。

import json from "./foo.json" with { type: "json" };
import("foo.json", { with: { type: "json" } });

https://github.com/tc39/proposal-import-attributes

またモジュールの依存に Top-Level Await を使った非同期モジュールが含まれないことをアサートする Module Sync Assert もこの提案を元にしています。

import { foo } from "./foo.mjs" with { assertSync: true };

属性は実行環境によってサポートするものが異なります。なお実行環境でサポートしていない属性を記述した場合は例外を投げることになっています。

締め

ざっくり Module Harmony をまとめてみました。インポートフェーズ修飾子の各提案についてはモチベーションはわかるものの、結構複雑でそれぞれの使い分けが難しそうですね。

今後どの様になるのか注視していこうと思います。

Discussion