0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ESMとCommonJSが混ざったら、Node.jsの判定と読み込み方法を分けて調べる

0
Last updated at Posted at 2026-10-08

Node.jsでrequire is not definedを見てimportへ直したら、今度は別のファイルで構文エラーになる。ESMとCommonJSが混在すると、構文の修正だけでは原因を追いにくくなります。

調べる順序は、まず「このファイルはどちらとして解釈されるか」、次に「相手をどの方法で読み込むか」です。この二つを分けると、package.jsonの変更、拡張子の変更、非同期読み込みへの変更を選びやすくなります。

Node.js v24.15.0で、小さなファイルを別プロセスから実行する10ケースを確認しました。bundler、TypeScript変換、独自loaderは使っていません。NODE_OPTIONSも子プロセスでは外しています。公式資料は2026年9月11日に確認し、最新版の表示はv26.8.2でした。実験結果はあくまでv24.15.0のものです。

最初に、拡張子と最寄りのpackage.jsonを見る

Node.jsのPackages公式説明では、.mjsはESM、.cjsはCommonJSです。.jsは最も近い親のpackage.jsonのtypeで明示できます。

この「最も近い」が効くため、リポジトリのルートだけを見ても判定できない場合があります。実験では、type: moduleのディレクトリの中に、type: commonjsを持つ子ディレクトリを作りました。

esm/
  package.json            type: module
  require.js
  legacy.cjs
  nested/
    package.json          type: commonjs
    legacy.js

require.jsの中でrequire()を呼ぶと失敗しますが、legacy.cjsとnested/legacy.jsではtypeof requireがfunctionになりました。外側がESMの領域でも、拡張子や内側の設定によってCommonJSとして解釈できます。

逆方向も確認しています。type: commonjsの中にある.jsへ静的なexportを書くと構文エラーになり、同じ領域の.mjsなら実行できました。

設定がないjsを、必ずCommonJSだとは考えない

typeを書かなければ昔と同じCommonJSになる、という理解も現在のNode.jsでは不十分です。公式資料には、判定が明示されていない入力にESM専用の構文があると、構文検出によってESMとして扱う動作が説明されています。

実験では、空のpackage.jsonを置いたディレクトリで次の.jsを実行しました。

export const value = 42;
console.log(value);

結果は42で、MODULE_TYPELESS_PACKAGE_JSONの警告も出ました。同じexportでも、明示したtype: commonjsの中では失敗し、typeがない場合は構文検出で成功しています。

警告を消そうとしてtype: moduleを追加すると、同じ領域にある従来の.jsも影響を受けます。先に対象ファイルと、その設定を共有するファイルを確認します。局所的な移行なら.mjsや.cjsで意図を明示する選択肢もあります。

なお、構文検出に伴う性能差は測っていません。この記事で確認したのは、実行の成否と警告です。

requireでESMを読める場合もある

「CommonJSからESMへは必ずimport()」という古い説明も、そのまま現在の環境へ当てはめられません。検証版に対応したCommonJSの公式資料では、require()から同期的なESMを読める条件が説明されています。

実験で読み込むESMは次の内容です。

export const value = 42;

CommonJS側からは、名前付きexportを返されたオブジェクトから読みました。

console.log(require("../esm/sync.mjs").value);

結果は42でした。.mjsという拡張子を見ただけで、require()が必ず拒否すると判断することはできません。

一方、対象を次のようにすると、同じ環境でもrequire()はERR_REQUIRE_ASYNC_MODULEで失敗しました。

await Promise.resolve();
export const value = 42;

違いはtop-level awaitです。公式資料では、読み込むモジュール自身だけでなく、依存先のモジュールグラフにtop-level awaitがある場合も、この同期読み込みの対象外です。今回の実験では読み込み先自身へawaitを置いて確認しています。

dynamic importは呼び出し元をESMへ変えない

CommonJSのファイルから、先ほどの非同期ESMを読みました。

import("../esm/async.mjs").then(m => console.log(typeof require, m.value));

出力はfunction 42です。ESMの準備を待って値を取得でき、呼び出し元には引き続きrequireがあります。import()を一つ書いたことで、CommonJSファイル全体がESMへ切り替わったわけではありません。

ただし、同期的に返していた処理をimport()へ置き換えるなら、その結果を待つ必要があります。読み込み箇所だけ書き換えても、呼び出し元が以前と同じ同期インターフェースで使えるとは限りません。設定の変更で済む問題か、非同期の境界が必要な問題かを分けます。

ファイルの判定が合っていても、パス解決で失敗する

ESMの相対importで./syncと書いたケースは、sync.mjsが隣にあってもERR_MODULE_NOT_FOUNDになりました。ESMの公式説明では、相対・絶対指定のimportに拡張子が必要です。

これは読み込み先のファイルがESMかCommonJSかとは別の問題です。今回なら./sync.mjsまで指定する必要があります。bundlerで省略できた書き方が、Node.jsへ直接渡したときも動くとは限りません。

確認した10ケースを並べると、次のようになります。

条件 結果
type: module内の.jsでrequire require is not defined
type: commonjs内の.jsで静的export 構文エラー
type: module内の.cjs CommonJSとして実行
type: commonjs内の.mjs ESMとして実行
内側にtype: commonjsを置いた.js 内側の設定で実行
CommonJSから同期ESMをrequire 値を取得
CommonJSからtop-level await付きESMをrequire ERR_REQUIRE_ASYNC_MODULE
CommonJSから同じESMをimport() 値を取得、requireも存在
ESMの相対importで拡張子を省略 ERR_MODULE_NOT_FOUND
typeなしの.jsへexportを書く 構文検出で実行、警告あり

再現コードは一時ディレクトリを作り、全ケースの終了値と出力をassertしてから削除します。既存プロジェクトのpackage.jsonを変えずに確認できます。

パッケージのexportsによる条件分岐、CommonJSの名前付きexport検出、TypeScriptの設定は今回の対象外です。混在時のエラーを調べる入口として、実行するNode.jsの版、対象ファイルの判定、読み込み方法、相対パスの順に確認します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?