目的
本記事ではNode.jsとTypeScriptを業務で利用しているエンジニア向けに、同じスクリプトでも実行環境によって動いたり動かなかったりする原因を調査した内容を共有します。
導入
シェルパ・アンド・カンパニー株式会社でプロダクト開発を担当しているkokiです。
このあいだ次のような不思議な現象が報告されました。
- もともと以下のコマンドを、各開発者がローカルで必要になったタイミングで実行していた。
npx ts-node ./test.ts
- ある日、1.のコマンドを実行するとエラーが出て実行できなくなっていた
- エラーメッセージ(一部):
Error [ERR_REQUIRE_ESM]: require() of ES Module
- エラーメッセージ(一部):
- 該当箇所周辺をコミットしていた人は
env-cmd -f ./.env.local npx ts-node ./test.tsというスクリプトで実行していた。この実行は成功する
最初は「ESMとCommonJSの違いについてtsconfigを見てけば良さそう」と思ってたのですがこの予想は外れ、なぜ env-cmd 経由だと動くのか、すぐには原因を突き止められませんでした。(※1)
結論
- Node.js 22系ではESMをrequireで読み込むことができる
- 最初は
node --experimental-require-module commonjs.cjsのようにフラグが必要だったが、22.12.0からデフォルトでもrequireで読みこめる
- 最初は
env-cmd経由で呼び出したts-nodeはNode.js 22.17.0を使うようになっていたenv-cmdはグローバルにインストールされており、プロジェクトとグローバルでnodeバージョンが違った- すなわち
npx ts-node ./test.tsはNode.js 20.17.0(プロジェクトのNode.jsバージョン)で実行され、env-cmd -f ./.env.local npx ts-node ./test.tsはNode.js 22.17.0(グローバルのNode.jsバージョン)で実行される
調査の流れ
結論は上記に書いた通りですが、何かの役に立つかもしれないのでどのように調べたのかを共有します。
tsconfig
まず「ESMにしか対応していないモジュールをうまく読めていない」ことは確定として良いと考えました。
そしてCommonJS、ES Moduleの違いと言えば tsconfig、トランスパイラの設定が思い浮かびました。(※2) ts-nodeのtsconfigはどこから読まれているのか?は以下のコマンドで確認できます。
npx ts-node --showConfig ./test.ts
env-cmd -f ./.env.local npx ts-node --showConfig ./test.ts
結果、2つの出力は同一でした… 読み込んでいるtsconfig.jsonの場所と内容は同一であることが確認できましたが、今回の原因とは関係なさそうです。
ts-node実行をdebugしてみる
次に、実行の仕方によってts-node側の処理が変わるかも?と当たりを付けました。
ChatGPTによると、以下のコマンドを実行するとモジュールの解決や読み込みの詳細ログが表示されるとのことでした。
NODE_DEBUG=module npx ts-node ./test.ts
NODE_DEBUG=module env-cmd -f ./.env.local npx ts-node ./test.ts
2つのコマンドの実行結果を比べると、色々違いすぎてよくわかりませんがバージョンが違いそうです!(※3)

env-cmdを経由するとバージョンが違う理由
グローバルにインストールした env-cmd を使用しているので、グローバルにインストールしたNode.jsのバージョンが使われているということでした。(※4)
そりゃそうだ…
Node.jsのバージョンが違うことが原因?
Node.jsのバージョンをローカルで変更して、エラーを再現することに成功しました。
あとはバージョンの違いについて調べて(※4)、Node.js 22系ではESMをrequireで読み込むことができることにたどり着きました。
-
参考1: https://nodejs.org/en/blog/announcements/v22-release-announce
If
--experimental-require-moduleis enabled, and the ECMAScript module being loaded byrequire()
22.12.0 からは --experimental-require-module フラグなしになっていました(※6)
-
参考2: https://nodejs.org/en/blog/release/v22.12.0
require(esm) is now enabled by default
まとめ
- Node.js 22系それ以前では、ESMを
requireする挙動に違いがある - 一見 tsconfig 設定の問題に見えたが、実際には Node.js のバージョン差異が原因だった
補足
-
※1 技術的な原因究明以外に絞って書きました。「ローカルで独自のスクリプトを流すような運用になってしまったこと」も解決するべきですね
-
※2 tsconfigの設定によってはts-nodeが動かないケースに遭遇したことがあり、その辺も調べるうえでノイズになりました...。今回は関係ありませんでした。
- 2023年末以降ts-nodeのリリースがなく(https://github.com/TypeStrong/ts-node/releases/tag/v10.9.2)、他のランタイムトランスパイラが良いという話も聞きます。新しいrepositoryではtsxでやってしまうことが多いですが何が正解なのかよくわかっておらずです
-
※3 スクショではnpmのバージョンが8.3.0となっています。Node.js 20.17.0 の npm のバージョンは v10.8.2 (https://nodejs.org/en/download/archive/v20.19.5) なので気になる方もいらっしゃるかもれませんが、プロジェクトで固定しているバージョンが8.3.0になってしまっており、今回の件の原因とは関係ありません。
-
※4 グローバルにインストールしたことに気づくまでは
env-cmdのソースコードを見て、spawnしたら見るパスが変わるのか…?などと調べたりしましてました。ここはもっと効率的に調べられた気がします。-
// Execute the command with the given environment variables const proc = spawn(command, commandArgs, { stdio: 'inherit', shell: options.useShell, env, })
-
-
※5 ChatGPTに聞くとまったく違う回答をするので、逆に調査が非効率になりました…。2024年ごろの情報はまだうまく返せないんでしょうか

ChatGPTにES Moduleを読み込めるか聞いた様子。requireでは読み込めないとのこと -
※6 Node.js 22.12.0 の変更後、v20.19.0 でも require(esm)機能が使えるようになっています。
-
https://nodejs.org/en/blog/release/v20.19.0#requireesm-is-now-enabled-by-default
require(esm) is now enabled by default
-
------------------------
シェルパ・アンド・カンパニーでは一緒に働く仲間を募集しています。