Node.js 22の--experimental-require-moduleで、NestJSからESM Onlyライブラリを使ってみる
はじめに
JavaScriptにはCommonJS / ECMA Script Modules(以下ESM)の二つのモジュールシステムがあり、ライブラリには両方サポートするもの(Dual Package)もあれば、ESMのみをサポートするものもあります。(本記事では後者をESM Onlyライブラリと呼称します。)
バックエンドフレームワークとして人気のあるNestJSはCommonJSの世界で動いており、ESM非対応です。具体的な問題としてNestJSではESM Onlyのライブラリを通常のimport
で使おうとするとERR_REQUIRE_ESM
が出ます。たとえば下記の記事が一例。
ESM Onlyのライブラリも増えてきている現状を反映し、NestJSのリポジトリにも数年に渡って複数のIssueが出ています。
NestJSの対応状況は、Pull Requestは出ているものの、現時点で保守コストと比べて得られる実際のメリットが見合わないので様子見、というのがここ数年のメンテナの一貫した姿勢のようです。
これまでの解決方法の一つはNestJSをES Moduleに書き換えるアプローチ。
ただ、import元のファイル名の拡張子を省略できず、ts
ファイルに対してjs
に書き換えたり、あまりやりたくない感じでした。
このようなところに、Node.js v22で--experimental-require-module
という実験フラグが追加されました。
これが解決するのが、まさにCommonJSからESMをrequireできない問題
、つまりがERR_REQUIRE_ESM
です。[1]
本当にそうなのか、--experimental-require-module
でNestJSでESM Onlyのライブラリを扱えるようになるのか、試してみるのがこの記事です。
この記事で扱うこと・扱わないこと
この記事では題名の通り、Node.js v22で導入された--experimental-require-module
を使い、NestJSでESM Onlyのライブラリを使えるかどうかを検証します。
CommonJS、ESMの歴史的経緯については深く扱いません(扱えません)。下記の記事などをご参考ください。
また、本記事の検証はERR_REQUIRE_ESM
の解消をもって良しとする簡単なものです。読んで字の如くexperimental
な機能のため、実務導入する場合はより厳密な検証が必要なことは言うを俟ちません。
--experimental-require-module
Node.js v22で追加されたオプション。これまでCommonJSからESMの読み込みは非同期のdynamic importに限られていたのをrequire(esm)
で同期的にできるようにするオプション。
内容については下記の記事がわかりやすく、一読を勧めます。
なお、順調に行けばNode.js v23ではフラグが外れる模様で、Pull Requestもmergeされていました。
Pull Requestの作成者がブログで経緯を説明しています。Node.jsの開発コミュニティについて筆者は疎いですが、実装が遅れた理由をculturalな要因に帰す点など面白く読めました。
やってみる
環境
- Macbook Air M1 (2020)
- バージョン管理: volta
Node.jsのバージョンを22に変更
$volta install node@22
執筆時点では22.9.0
がインストールされました。
プロジェクト作成
$npm install -g @nestjs/cli
$nest new nest-esm-only-test
$cd nest-esm-only-test
パッケージマネジャーにはnpm
を選択しました。
ESM Onlyのモジュールを使ってエラーを確認
下記の記事に揃えて、ESM Onlyなchalk
で検証。chalk
はログに色を付けてくれます。[2]
$npm install chalk
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
+ "chalk": "^5.3.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
Version 5以降であればESM Onlyです。
AppService
でchalk
を読み込みます。
import { Injectable } from '@nestjs/common';
import chalk from 'chalk';
@Injectable()
export class AppService {
getHello(): string {
console.log(chalk.blue('Hello World!'));
return 'Hello World!';
}
}
これで実行してみます。TypeScriptがCommonJSにトランスパイルされ、chalk
がrequire
されるでしょう。青色の「Hello World!」の文字をログに出すのが想定の挙動です。
$npm start
ERR_REQUIRE_ESM
が出ました。
Error [ERR_REQUIRE_ESM]: require() of ES Module
~~~~/nest-esm-only-test/node_modules/chalk/source/index.js from
~~~~/nest-esm-only-test/dist/app.service.js not supported.
Instead change the require of index.js in ~~~~/nest-esm-only-test/dist/app.service.js to a dynamic import() which is available in all CommonJS modules.
at TracingChannel.traceSync (node:diagnostics_channel:315:14)
at Object.<anonymous> (~~~~/nest-esm-only-test/dist/app.service.js:11:17) {
code: 'ERR_REQUIRE_ESM'
}
Node.js v22.9.0
dynamic importで非同期で読み込むように提案されています。
--experimental-require-module
フラグを追加して実行
package.json
のscripts
を修正。
+ "build": "NODE_OPTIONS='--experimental-require-module' nest build",
+ "start": "NODE_OPTIONS='--experimental-require-module' nest start",
+ "start:dev": "NODE_OPTIONS='--experimental-require-module' nest start --watch",
+ "start:debug": "NODE_OPTIONS='--experimental-require-module' nest start --debug --watch",
+ "start:prod": "NODE_OPTIONS='--experimental-require-module' node dist/main",
と変更した上で下記を実行。
$npm start
するとアラートを表示しながらも立ち上がりました。
http://localhost:3000/
にGetでアクセスしてみます。
さらにターミナルを見てみると...
青字で表示されていました。簡単な例ですが、動作するようになりました。
おわりに
以上、Node.js v22で導入された--experimental-require-module
を使い、NestJSでESM Onlyのライブラリを使うことができました。
この問題はNestJSを避ける理由の一つではあったので、もし--experimental-require-module
でこのまま大きな問題なく解消するのであれば喜ばしいです。が、NestJSはその他にもDecoratorの仕様が古いといった問題が残ります。こちらも2023年のIssueを見る限り対応する素ぶりはありません。
それはさておき、NestJSに限らず他のライブラリでも、ESM対応で困るケースはあるでしょうから、--experimental-require-module
は光明となるかもしれません。
サンプルコードリポジトリ
Related
調べる中で参考になった文献。
ESMとCommonJS
ライブラリ作者は2つの対応が必要だった。(Dual Packages)
--experimental-require-module
実装者のブログ。
"ESM is async, CJS is sync, so CJS cannot load ESM“
という思い込みが実装を遅らせた、という意見。
[...] I found out by chance that ESM itself was not actually designed to be unconditionally asynchronous. Rather, it was designed to be only conditionally asynchronous - only when the graph contains top-level await.
Then it would seem very natural for require() to at least support ESM graphs that contains no top-level await. While some libraries might have valid reasons to use top-level await, it’s probably not that common a thing [...]
逆にいえば、top-level await
が入っているケースはこれまで通りrequireできない。
フラグはNode.js 23で外れる予定。Pull Requestがmergeされていた。
NestJS
IssueにMaintainerの見解が書かれている。メリットと保守コストを衡量して、現状は対応しない方針。
--experimental-require-module
は大いに歓迎しているようで、このまま対応はないかもしれない。
現時点でドキュメントにESM対応についての項目はないが、Issueが出ている。
TypeScriptのDecoratorの変遷
JestのESM対応
Jestについて似たような記事があった。
-
従来でもCJSでESMを読み込めない訳ではなく、
dynamic import
を用いて非同期で読み込むことはできる。ただ、NestJSのIssueでもコメントがあるが、async/await
の追加など面倒が伴う。 ↩︎ -
ちなみに今ではNode.jsでもログに色をつけられるようになっている。参考: https://zenn.dev/morinokami/articles/npm-uninstall#chalk ↩︎
Discussion