Fork me on GitHub

Deep Dive: Node.jsのESMデフォルト化への道

Edit on GitHub 編集履歴を見る

Node.js 21では --experimental-default-type=module フラグで、JavaScriptファイルのデフォルトの解釈をCJS(CommonJS)からESM(ECMAScript Modules)に変更できるようになっています。

これは、Node.jsにおいてJavaScriptファイル(.js)のデフォルトをESMに変更するための第一歩です。

今回のDeep Diveでは、Node.jsのESMデフォルト化に向けたIssueや実装について紹介します。

Node.jsのESMデフォルト化

Discussion: New “ESM by default” mode · Issue #49432 · nodejs/node

このIssueは、Node.jsにおけるambiguous fileの解釈をCJS(CommonJS)からESM(ECMAScript Modules)へと変えたいというDiscussion Issueです。

Ambiguous file(あいまいなファイル)とは、次のものを指しています。

  • .js ファイル かつ package.jsontype が定義されていない
    • .mjsはESMとして扱われるためambiguous fileではない
  • node --eval のように文字列入力されていて、 --input-type が指定されていない

現在のNode.jsは、あいまいなファイルをCommonJSとして解釈して実行します。

このIssueでは、あいまいなファイルをESMとして解釈して実行する方法について議論されています。

  1. node バイナリを分ける
    • バイナリの管理コストが発生する
  2. package.jsonを生成するときに type=module を追加してもらう
    • package.jsonを使わない”スクリプト”の問題やチュートリアルといった記事の説明コストが発生する
  3. デフォルトの解釈を変更するフラグを追加する

などのオプションが話され、実験的な機能としてデフォルトの解釈を変更する --experimental-default-type が Node.js 21に向けて実装されました。

esm: --experimental-default-type flag to flip module defaults by GeoffreyBooth · Pull Request #49869 · nodejs/node

このPRでは、node --experimental-default-type=module で実行された場合、ambiguous file(あいまいなファイル)をESMとして解釈して実行できるフラグが実装されています。

📝 このフラグはNode.js 21.0.0に含まれています。

When to make --default-type=module the Node.js default · Issue #1445 · nodejs/TSC

このIssueは--experimental-default-type=module がNode.js 21に向けて実装されるので、Node.js 21をリリースするときに、それをいつデフォルトにするかという話をするために、将来の方向性について議論しているIssueです。

このIssueでは、--experimental-default-type=module というデフォルトの解釈を変えるフラグだけでは、大規模な破壊的な変更となり混乱を生むという問題が指摘されています。

たとえば、 node_modules/ 以下のパッケージのデフォルト解釈をESMに変えた場合動かないパッケージが多くあり、すでにメンテナンスされていないパッケージもあるため、立ち往生してしまうなどの問題が指摘されています。(アプリケーションコードとパッケージの作者が異なるため、パッチを当てる方法がなくなるという問題)

そのため、デフォルトの解釈を切り替えるだけの--experimental-default-type=module のようなフラグだけでは、移行パスが不十分で分断が発生するという問題があります。この0か1の問題に対して、次のようなIssueが出されています。

Proposal: Set --experimental-default-type mode by detecting ESM syntax in entry point · Issue #50043 · nodejs/node

このIssueでは、node_modules/ 以下のファイルの中身を見てCJSかESMを判定する --experimental-detect-module のようなフラグを追加するのはどうかという提案です。

esm: detect ESM syntax in ambiguous JavaScript by GeoffreyBooth · Pull Request #50096 · nodejs/node

このPRでは、--experimental-detect-moduleが実装されています。
この--experimental-detect-moduleの実装では、node_modules/ 以下のあいまいなファイルの中に、ESMの構文が含まれているならそれをESMとして扱い、そうではない場合はCJSとして扱います。

ESMの構文は import / export / import.meta などの静的に解釈できるものをV8を使って判定しています。( import() はCommonJSでも利用できるため、ESMの構文としては扱われていません)

現在のデフォルトはCJSであり、ambiguous fileにESMの構文が含まれている場合は実行時にエラーとなります(これはacornを使ってESMの構文が含まれているかを判定しています)

Node.js 20でESMの構文を含むCJSを実行した時のエラーは次のようになっています。

import lodash from "lodash"
^^^^^^

SyntaxError: Cannot use import statement outside a module

そのため、ESMの構文を含むCJSというものはないという前提にでき、--experimental-detect-module は破壊的な変更をなく入れることができるのではないかという話がされています。(すでに実行できているものが実行できなくなることはないという点)

このアプローチのデメリットは、ファイルの中身を見てESMかを判定するためパフォーマンスが悪くなるのではという点があります。

おわりに

まだ、Node.jsにおいて .js ファイルをESMとして扱えるようにするかをどういう方法でやるかは確定はしていません。

開発バージョンであるNode.js 21で --experimental-* でフラグを実装しながら、互換性的な問題がないかやパフォーマンスが問題ないかなどを調べて進んでいくと思われます。

関連

tc39/proposal-UnambiguousJavaScriptGrammar

2016年から2017年にかけて、Node.jsがTC39(ECMAScriptの仕様策定をするグループ)で、ファイルの中身を見てScriptかModuleかを判定できるようにするProposalを出していました。
ここでいうScriptはCommonJSで、ModuleがESMです。

このあいまいなファイルの判定としてあげていたのが import 文や export 文がファイルに含まれているかどうかでした(また "use module" のようなDirectiveの話も行われていました)

この提案は、ECMAScriptの仕様ではなくプラットフォーム側(ブラウザやNode.js)でやるべきことであるとして、TC39としてのコンセンサスは取れずにそこで議論は終了しています。

2016-2017年の段階で、Node.jsはESMサポートをする方針として次の3つを持っていました

  1. .js の中身を見てCJSかESMかを判定する
  2. package.jsontype などの特定のフィールドで判定する
  3. .mjs のような拡張子で判定する

TC39で提案していたものは1に関するもので、このとき(2016-2017年)は特に進むことはありませんでした。
Node.js 20時点は2と3が実装済みで、今回の --experimental-default-type=module は2016-2017年に提案していた 1の内容と近いものだと考えられます。

この記事へ修正リクエストをする
JSer.info Slackに参加する