🎶

ECMAScript Module Harmony

2024/01/04に公開
変更情報

【2024/08/20】

【2024/05/26】


好きなTC39 Proposals発表ドラゴン(ニコニコ動画YouTube

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;
}

ソースフェーズインポート

フェッチ、単一ファイルのコンパイルまで行うのがソースフェーズインポートです。

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;

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

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

  • WebAssembly.instantiateStreaming を使わないですみ、フロントエンドとサーバーサイド JavaScript で同一の記述ができる
  • モジュールの依存に含めることができ、バンドル時に最適化ができるようになる
  • ブラウザ環境において Content Security Policy の unsafe-eval (wasm-unsafe-eval) が不要になる

なお、これは今まで通り WebAssembly がインポートする依存函数を JavaScript から実行時に指定する方式です。ソースフェーズインポートとは別に WebAssembly 内部で直接依存モジュールを記述する提案については WasmCG で進行しています。

import { foo } from "./foo.wasm";

https://github.com/WebAssembly/esm-integration/

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" });

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

同一の 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" });

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

モジュール宣言

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

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

import { foo } from Foo;

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

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

遅延インポート評価

リンクまで行うのが遅延インポート評価です。ネームスペースインポートのみサポートしています。

最初にネームスペースオブジェクトにアクセスされるまでモジュールの実行を遅延させることで、パフォーマンス向上を図ることができます。

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

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

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

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" } });

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

属性は実行環境によってサポートするものが異なります。Web 標準においては CSS ModulesText/Bytes Modules が提案されています。なお実行環境でサポートしていない属性を記述した場合は例外を投げることになっています。

モジュール同期アサート

モジュールの依存に Top-Level Await を使った非同期モジュールが含まれないことをアサートします。

"assert sync";

// Top-Level Await を使った非同期モジュールが含まれないかアサートする
import { foo } from "./foo.mjs";

https://github.com/tc39/proposal-module-sync-assert

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');

https://github.com/tc39/proposal-compartments

締め

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

引き続き今後どの様になるのか注視していこうと思います。個人的に Scrapbox にて TC39 meeting で決まったことをまとめています。よければこちらも御覧ください。

https://scrapbox.io/petamoriken/

Discussion