シェルパ・アンド・カンパニー株式会社 エンジニアブログ

シェルパ・アンド・カンパニー株式会社のエンジニアが技術情報を発信します

同じtsconfigでもESMが読み込めないエラーがでたりでなかったり

目的

本記事ではNode.jsとTypeScriptを業務で利用しているエンジニア向けに、同じスクリプトでも実行環境によって動いたり動かなかったりする原因を調査した内容を共有します。

導入

シェルパ・アンド・カンパニー株式会社でプロダクト開発を担当しているkokiです。

このあいだ次のような不思議な現象が報告されました。

  1. もともと以下のコマンドを、各開発者がローカルで必要になったタイミングで実行していた。
    • npx ts-node ./test.ts
  2. ある日、1.のコマンドを実行するとエラーが出て実行できなくなっていた
    • エラーメッセージ(一部): Error [ERR_REQUIRE_ESM]: require() of ES Module
  3. 該当箇所周辺をコミットしていた人はenv-cmd -f ./.env.local npx ts-node ./test.tsというスクリプトで実行していた。この実行は成功する
    1. 補足: env-cmd環境変数を読み込んでコマンドを実行するツールです。環境変数は本筋とは関係ありませんでした。

最初は「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)

npxの実行とenv-cmd付きの実行のdiffをとった様子

 

env-cmdを経由するとバージョンが違う理由

グローバルにインストールした env-cmd を使用しているので、グローバルにインストールしたNode.jsのバージョンが使われているということでした。(※4)

そりゃそうだ…

Node.jsのバージョンが違うことが原因?

Node.jsのバージョンをローカルで変更して、エラーを再現することに成功しました。

あとはバージョンの違いについて調べて(※4)、Node.js 22系ではESMをrequireで読み込むことができることにたどり着きました。

22.12.0 からは --experimental-require-module フラグなしになっていました(※6)

まとめ

  • 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したら見るパスが変わるのか…?などと調べたりしましてました。ここはもっと効率的に調べられた気がします。

  • ※5 ChatGPTに聞くとまったく違う回答をするので、逆に調査が非効率になりました…。2024年ごろの情報はまだうまく返せないんでしょうか

    ChatGPTにES Moduleを読み込めるか聞いた様子。requireでは読み込めないとのこと
  • ※6 Node.js 22.12.0 の変更後、v20.19.0 でも require(esm)機能が使えるようになっています。

 

------------------------

シェルパ・アンド・カンパニーでは一緒に働く仲間を募集しています。

https://herp.careers/v1/cierpa0905