🧪

Node.js 22の--experimental-require-moduleで、NestJSからESM Onlyライブラリを使ってみる

2024/10/11に公開

はじめに

JavaScriptにはCommonJS / ECMA Script Modules(以下ESM)の二つのモジュールシステムがあり、ライブラリには両方サポートするもの(Dual Package)もあれば、ESMのみをサポートするものもあります。(本記事では後者をESM Onlyライブラリと呼称します。)

バックエンドフレームワークとして人気のあるNestJSはCommonJSの世界で動いており、ESM非対応です。具体的な問題としてNestJSではESM Onlyのライブラリを通常のimportで使おうとするとERR_REQUIRE_ESMが出ます。たとえば下記の記事が一例。

https://zenn.dev/nao50/articles/nestjs-in-angular18-with-ssr#発生するエラー

ESM Onlyのライブラリも増えてきている現状を反映し、NestJSのリポジトリにも数年に渡って複数のIssueが出ています。

https://github.com/nestjs/nest/issues/13319

https://github.com/nestjs/nest/issues/13817

https://github.com/nestjs/nest/issues/13851

NestJSの対応状況は、Pull Requestは出ているものの、現時点で保守コストと比べて得られる実際のメリットが見合わないので様子見、というのがここ数年のメンテナの一貫した姿勢のようです。

これまでの解決方法の一つはNestJSをES Moduleに書き換えるアプローチ。

https://zenn.dev/shuhei_takada/articles/a7531731a7cf04

ただ、import元のファイル名の拡張子を省略できず、tsファイルに対してjsに書き換えたり、あまりやりたくない感じでした。

このようなところに、Node.js v22で--experimental-require-moduleという実験フラグが追加されました。

https://nodejs.org/en/blog/announcements/v22-release-announce

これが解決するのが、まさにCommonJSからESMをrequireできない問題、つまりがERR_REQUIRE_ESMです。[1]

本当にそうなのか、--experimental-require-moduleでNestJSでESM Onlyのライブラリを扱えるようになるのか、試してみるのがこの記事です。

この記事で扱うこと・扱わないこと

この記事では題名の通り、Node.js v22で導入された--experimental-require-moduleを使い、NestJSでESM Onlyのライブラリを使えるかどうかを検証します。

CommonJS、ESMの歴史的経緯については深く扱いません(扱えません)。下記の記事などをご参考ください。

https://zenn.dev/yodaka/articles/596f441acf1cf3#es-modulesに全振りする上での問題点

https://zenn.dev/uhyo/articles/typescript-module-option#module%3A-node16であらわになったcjsとesmの問題

また、本記事の検証はERR_REQUIRE_ESMの解消をもって良しとする簡単なものです。読んで字の如くexperimentalな機能のため、実務導入する場合はより厳密な検証が必要なことは言うを俟ちません。

--experimental-require-module

Node.js v22で追加されたオプション。これまでCommonJSからESMの読み込みは非同期のdynamic importに限られていたのをrequire(esm)同期的にできるようにするオプション。

https://nodejs.org/en/blog/announcements/v22-release-announce?ref=blog.arcjet.com#support-requireing-synchronous-esm-graphs

内容については下記の記事がわかりやすく、一読を勧めます。
https://hiroppy.me/blog/nodejs-new-module-algorithm/

なお、順調に行けばNode.js v23ではフラグが外れる模様で、Pull Requestもmergeされていました。
https://github.com/nodejs/node/pull/55085

Pull Requestの作成者がブログで経緯を説明しています。Node.jsの開発コミュニティについて筆者は疎いですが、実装が遅れた理由をculturalな要因に帰す点など面白く読めました。

https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/

やってみる

環境

  • 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]

https://zenn.dev/shuhei_takada/articles/a7531731a7cf04

$npm install chalk
package.json
  "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です。
https://github.com/chalk/chalk/blob/4a10354857ba6d7932dad5fa6ef2e021c4ed47fb/readme.md?plain=1#L42

AppServicechalkを読み込みます。

app.service.ts
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にトランスパイルされ、chalkrequireされるでしょう。青色の「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.jsonscriptsを修正。

package.json
+    "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を見る限り対応する素ぶりはありません。

https://github.com/nestjs/nest/issues/11414

それはさておき、NestJSに限らず他のライブラリでも、ESM対応で困るケースはあるでしょうから、--experimental-require-moduleは光明となるかもしれません。

サンプルコードリポジトリ

https://github.com/HosakaKeigo/nest-esm-only-test

調べる中で参考になった文献。

ESMとCommonJS

https://zenn.dev/uhyo/articles/typescript-module-option#module%3A-node16であらわになったcjsとesmの問題

https://zenn.dev/yodaka/articles/596f441acf1cf3#es-modulesに全振りする上での問題点

ライブラリ作者は2つの対応が必要だった。(Dual Packages)
https://blog.cybozu.io/entry/2020/10/06/170000

--experimental-require-module

https://blog.arcjet.com/nodejs-22-support-esm-require-for-nestjs/

https://hiroppy.me/blog/nodejs-new-module-algorithm/

実装者のブログ。

https://joyeecheung.github.io/blog/2024/03/18/require-esm-in-node-js/

"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されていた。
https://github.com/nodejs/node/pull/55085

NestJS

IssueにMaintainerの見解が書かれている。メリットと保守コストを衡量して、現状は対応しない方針。
https://github.com/nestjs/nest/issues/13319#issuecomment-2022145229

--experimental-require-moduleは大いに歓迎しているようで、このまま対応はないかもしれない。
https://x.com/kammysliwiec/status/1770554561016545340

現時点でドキュメントにESM対応についての項目はないが、Issueが出ている。
https://github.com/nestjs/docs.nestjs.com/issues/3093

TypeScriptのDecoratorの変遷

https://zenn.dev/pixiv/articles/ab9a7d7f654a79#おわりに

JestのESM対応

Jestについて似たような記事があった。
https://qiita.com/toydev/items/e163d35a7e8e3c11fba2#3-背景

脚注
  1. 従来でもCJSでESMを読み込めない訳ではなく、dynamic importを用いて非同期で読み込むことはできる。ただ、NestJSのIssueでもコメントがあるが、async/awaitの追加など面倒が伴う。 ↩︎

  2. ちなみに今ではNode.jsでもログに色をつけられるようになっている。参考: https://zenn.dev/morinokami/articles/npm-uninstall#chalk ↩︎

Discussion