VercelのNode.jsランタイムでのみERR_REQUIRE_ESMが発生したので、原因調査と解決を行なった

はじめに

こんにちは。フロントエンドエンジニアをしているryoです。今回はVercelのNode.jsランタイムでのみERR_REQUIRE_ESMが発生するという、ちょっと不思議な(でもインターネットを見ると結構遭遇していそうな?)経験をしたのでそれをまとめます。同じ事象で困っていたり、原因に興味がある方の一助になれば幸いです。
では書いていきます。

忙しい人向け

  • 原因:
    • VercelのNode.jsランタイムでは、Node.jsのv22.12.0からデフォルトで有効になっているはずのrequire(esm)の機能が無効化されている。(2025/12/30現在)
      • Node.jsのバージョンに依らず、v22.x,v24.xどちらもそうなっている
    • jsdomの v27.0.1 からNode.jsの minimum version が上がってしまった。これによりjsdomの依存しているパッケージに、requrire(esm)が有効である前提のパッケージが混在してしまった。
  • 解決:
    • jsdomから、require(esm)が有効でなくても動作するfast-xml-parserに移行
    • --no-experimental-require-module を利用して検証するようにした

きっかけ

自身で作っているNext.jsのアプリケーションのメンテナンスのため、パッケージのアップデートをしていました。
Vercelのデプロイも完了して数日経った頃、xmlのパースを行う機能を含むエンドポイントで500エラーになっている事に気づき、ログを見てみるとERR_REQUIRE_ESMで内部エラーになっている事が分かりました。

原因

調べてみて分かった原因を整理します。

jsdomをv27.0.1以降のバージョンに上げた事で、require(esm)を前提とするパッケージが混在してしまった

パッケージのメンテナンスの中で、jsdomのパッチバージョンを上げていました。
v27.0.1のリリースノートには、このバージョンで、意図せずNode.jsの minimum versionが上がってしまったという旨の警告が記載されていました。
これにより、require(esm)がデフォルトで有効である前提の依存関係が含まれるようになっていました。

require(esm)とは

同期的なESM(Top-level await ではないESM)に限り、CJSからrequire()で読み込む事が出来るようになる機能です。
この機能以前は、CJSからESMを読み込む際は、dynamic import(await import(hoge.mjs)のようなやり方)する必要がありました。
なのでライブラリやアプリの開発者は、CJSで実装する際、require()しようとしているライブラリがCJSなのかESMなのか知る必要があり、混在した運用が難しい状態でした。
また、ライブラリ開発者は、ユーザーのためにCJS・ESMそれぞれに対応したコードを保守・運用する必要(いわゆるdual module)も発生していました。
そこで提案されたのがrequire(esm)という機能で、簡単に言うと、Top-level await なモジュールでなければ、同期的に読み込む事が出来るため、CJS上でrequire(esm)する事は自然に出来るはずだという提案です。実際その通りに実装されました。
ちなみに、そもそもCJSは同期的、ESMは非同期的という思想の違いがあります。それぞれが策定された経緯も含めて興味深いので、また別でブログにまとめようと思います。
更にちなみに、require(esm)の機能が有効でない時、CJSでrequire()を使ってESMを読み込もうとして発生するエラーが今回の件で発生していたERR_REQUIRE_ESMです。

VercelのNode.jsランタイムのrequire(esm)が無効化されていた

執筆時現在、VercelのNode.jsランタイムだけrequire(esm)の機能が無効化されている事が分かりました。
調査方法と結果を整理して記載します。

調査方法

  1. ランタイム検証用APIを作成する
    ローカルやVercelのプレビュー環境でランタイム時のrequire(esm)の設定を確認するために、以下のAPIを作成しました。
export async function GET() {
	return Response.json({
		nodeVersion: process.version,
		requireEsmEnabled: process.features?.require_module ?? false,
		timestamp: new Date().toISOString(),
	});
}
  1. ビルドプロセスで通る箇所に以下のconsole.logを入れる
console.log(`requireEsmEnabled: ${process.features?.require_module}`);

各環境のrequire(esm)調査結果

以下が調査結果です。

環境Node.js バージョンrequireEsmEnabled (ビルドプロセス)requireEsmEnabled (ランタイム)
ローカルv24.12.0truetrue
Vercelv22.xtruefalse
Vercelv24.xtruefalse

上記結果から分かる通り、Vercelのランタイム時のみ false になっていました。もしBuildプロセスでrequire(esm)がfalseになっていたら、ビルドが失敗するので気づく事が出来ていたと思います。

結論

つまり、Vercel上でrequire(esm)が無効になっていて、jsdomのv27.0.1以降がその機能を必須とする状態になってしまっていたから、今回の事象が発生していました。

対応

以下の対応をしました。

  1. jsdomから、require(esm)が有効でなくても動作するfast-xml-parserに移行
    require(esm)が無効であっても、動作するライブラリとして、fast-xml-parserに移行しました。そもそも、私の場合はXMLをパースする事が目的だったので、jsdomはfatな選択でした。
  2. --no-experimental-require-module を利用して検証するようにした
  3. package.jsonに"type": "module"を追加し、プロジェクトを明示的にESMで統一した
    これについては今回の事象の解決にはならないですが、ESMで統一して運用していた方が楽かな、と思いこの設定を追加しました。

終わりに

今回はVercel上のNode.jsランタイムで起きたERR_REQUIRE_ESMの原因調査と解決までをまとめてみました。
同じようなことで困っていたり、原因を知りたい人の理解の一助になれば幸いです。
最後まで読んでいただきありがとうございました!