🗼

Babel に JS Module Blocks プロポーザルを実装する

2020/12/11に公開

Node.js アドベントカレンダー 2020の 11 日目です。Node.js についての話題が思いつかなかったのでもっと広く、JavaScript についての記事を書きます。

はじめに

2020 年 11 月に行われた TC39 のミーティングで JS Module Blocks というプロポーザルが Google の Surma 氏と Igallia の littledan 氏によって提案され、Stage 1 になりました。

Babel は様々な ECMAScript のプロポーザルを先取りして実装しており、このプロポーザルも例外ではありません。そしてその実装を筆者が担当することになりました。

この記事では JS Module Blocks プロポーザルの現時点での仕様、そして Babel にそれを実装ことになった経緯と実際の手順について解説します。

JS Module Blocks とは

前述の通り、JS Module Blocks は最近 Stage 1 になったばかりの ECMAScript のプロポーザルです。

このプロポーザルは、ひとことで言えば「ファイルをまたがずにモジュールを定義する手段」を提供します。

具体的な例を見てましょう。

const m = module {
  export const foo = "foo";
};

例の中の module {...} の部分が、この提案で新しく導入される構文インラインモジュール式(InlineModuleExpression)です。厳密に言えば、このInlineModuleExpressionを ECMAScript のPrimaryExpressionに追加します。

module {...} の中身は、それが配置されているモジュールとは別のモジュールとして定義されます。

そして、この式によって定義されたモジュールは dynamic import(import(...))で import できます。

const m = module {
  export const foo = "foo";
};

const module = await import(m);
console.log(module.foo); // foo

今までモジュールを定義するためにはファイルをまたぐ必要がありましたが、インラインモジュール式を使うことで1つのファイル内で複数のモジュールを定義することができるようになります。

さて、この機能はどんなときに役に立つのでしょうか。

プロポーザルの著者である Surma 氏が推しているのは Web Worker とのインテグレーションです。

インラインモジュール式を使えば、次のようにして Web Worker を使うことができます。

const worker = new Worker(module {
  onmessage = function({data}) {
    postMessage("データを受け取りました: ", data);
  }
}, { type: "module" });

worker.onmessage = ({data}) => alert(data);
worker.postmessage("データ");

今まで Web Worker を使う方法としては、別のファイルに Worker の処理を書いて Worker コンストラクタにファイルを渡すか、Worker 内の処理を data url にしてから Worker コンストラクタに渡す方法がありました。

そのどちらもそこそこに冗長です。

JS Module Blocks プロポーザルが導入されれば、より簡単に Worker を使えるようになるでしょう。

現時点ではブラウザの Worker のことを想定して設計されているようですが、Node.js の Worker threads などに対して使えるようになったら更に便利になりそうです。

なお、JS Module Blocks は現在まだ Stage 1 なので今後仕様が変更される場合がある点には注意してください。

Babel に JS Module Blocks を実装する

経緯

まず、筆者が Babel に JS Module Blocks を実装することになった経緯を説明します。

筆者はもともと Babel チームに所属しており、主にパーサーのバグ修正やコードレビューを担当しています。そして Babel の開発にこれからも貢献していきたいと考えています。

Babel はパーサーだけではなく、様々なパッケージの monorepo として構成されています。なので、Babel の開発に更に貢献していくためには monorepo 内の他のパッケージについても把握する必要があると前々から感じていました。

そんなときに、TC39 の 11 月のミーティングでなにやら面白そうなプロポーザルが Stage 1 になったというツイートを見かけました。

個人的に Worker の使い勝手の悪さは以前から感じており、それを解決しうるこのプロポーザルには興味を引かれました。

さらに、新しいプロポーザルを Babel に実装するときはパーサーだけではなく babel-generator や babel-types などのパッケージに触る必要があるため、Babel について更に知るためのきっかけになるのではないかと考えました。

そこで Babel のコアメンテナーの一人に、「このプロポーザルの Babel への実装を自分にやらせてもらえないか」と思い切って尋ねてみました。そしたら「プロポーザルの著者に確認がとれたらいいよ」というような返答をもらい、数日後に「確認とれたから実装着手して大丈夫だよ!」という連絡がきました。

そのような経緯があって、JS Module Blocks プロポーザルを Babel に実装しています。

実装する

実装しても良いということになったので、さっそく始めましょう。JS Module Blocks は、Babel のみでトランスフォームするのは難しい(できない)ので、パースのサポートのみを実装します。(ちなみに、Import Assertions とかもパースのみサポートされています。)

まずはパーサーから始めます。パーサーを実装するためには、最初に AST の仕様を決める必要があります。

モジュール式なので、無難に名前は ModuleExpression で良いでしょう。

そして、モジュール式のブロックの中身は普通のブロックと同様に Statement が並ぶように見えます。

それらを考えた結果、AST の仕様は次のようにしました。

interface ModuleExpression <: Expression {
  type: "ModuleExpression";
  body: [ Statement ];
}

AST の仕様が決まったら、次はこの構文をパースできるようにしていきます。

module という文字列を持つトークンをを見つけたら最初の分岐に入ります。日本語を含む疑似コードで書くとこんな感じになります。

if (現在のトークンが"module") {
}

そしたらこれが Identifier ではなくモジュール式であることを確認するために1つ先読みをします。

if (現在のトークンが"module") {
  if (1つ先のトークンが"{") {
  }
}

さらに、モジュール式はmodule{の間に改行を含むことができないので、"{" の前に改行がないことをチェックする必要があります。

if (現在のトークンが"module") {
  if (
    1つ先のトークンが"{" &&
    1つ先のトークンの前に改行が存在しない
  ) {
  }
}

ちなみにこの制約がない場合、次のような既存の JavaScript の解釈と衝突してしまいます。

module
{}

これらのチェックを通ったなら、if 文のボディ内ではモジュール式のパースを開始していいでしょう。

実はモジュール式のパース自体はかなり簡単です。ブロックの中身をパースする関数がすでに存在するので、それを呼んであげれば簡単に実装できます。

これらの実装ができたらテストを書きます。babel-parser は基本的にスナップショットテストなので、ざっとモジュール式を使うコードをいくつか書いて AST のスナップショットを作成します。

次は AST の spec のドキュメントを更新します。それでパーサーの実装は完了です。

パーサーの実装が終わったら babel-generator の実装に入ります。

他のノードのための実装を参考にしたらすぐにできました。(今回は構文的に似ている Class Static Blocks の実装を参考にしました。)

export function ModuleExpression(node: Object) {
  this.word("module");
  this.space();
  this.token("{");
  if (node.body.length === 0) {
    this.token("}");
  } else {
    this.newline();
    this.printSequence(node.body, node, {
      indent: true,
    });
    this.rightBrace();
  }
}

babel-generator の実装が終わったら次は babel-types に ModuleExpression を追加します。

こんなコードを書いて、ビルドするとその他の必要がコードが自動で生成されます。

// https://github.com/tc39/proposal-js-module-blocks
defineType("ModuleExpression", {
  visitor: ["body"],
  fields: {
    body: {
      validate: chain(
        assertValueType("array"),
        assertEach(assertNodeType("Statement"))
      ),
    },
  },
  aliases: ["Expression"],
});

これも、他のノードの定義を参考にしたらすぐできました。

最後に、https://github.com/babel/babel/blob/main/CONTRIBUTING.md#creating-a-new-plugin-spec-new に従ってシンタックスプラグインを作成し standalone に微修正を加えて完了です。

あまり実装の詳細を長くしても Babel に詳しい人以外面白くないと思うのでかなり駆け足で紹介しましたが、もし興味がある場合は実際の Pull Requestを見てください。

ちなみにまだ Pull Request を出したばかりでレビューをもらっている段階で、ここで紹介した実装はいくつか変わりそうです。

それに、実は JS Module Blocks の構文の仕様もまだ厳密には定まっていないので、マージにはまだ時間がかかりそうです。

おわりに

Babel チームに所属してから 2 ヶ月くらい経ちますが、そこそこ大きめな機能追加を実装したのは初めてで、楽しくやることができました。ただ、プロポーザルの著者である surma と littledan に Pull Request でメンションを飛ばしましたが、それはとても緊張しました...

アドベントカレンダーとしては Node.js に直接関係しない内容になってしまい申し訳ないですが、この記事を呼んだ誰かが Babel への貢献に興味を持ってくれたら嬉しいです。

Discussion