Node で .js を実行したら、次のエラーで落ちることがあります。
ReferenceError: exports is not defined in ES module scope
Node は続けて、原因のヒントも出します。
This file is being treated as an ES module because it has a '.js' file extension and '.../package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
つまり、そのファイルが ES Module(ESM)として扱われているのに、CommonJS の exports(や module.exports)を使っているのが原因です。自分では exports なんて書いていない、というときは、TypeScript のコンパイル結果がこの形になっているケースが典型です(後述)。
なぜ出るのか(ESM スコープに CommonJS の変数は無い)
ESM 扱いのファイルには、CommonJS が用意していた exports / module / require / __dirname / __filename が存在しません。exports.greeting = ... は「未定義の変数 exports に代入しようとした」= ReferenceError になります。ファイルが ESM 扱いになる条件は次のとおりです。
| 条件 | ファイルの扱い |
|---|---|
近い package.json に "type": "module" がある .js | ESM |
拡張子が .mjs | ESM(常に) |
種別未確定の .js に import/export などの ESM 構文がある(Node 20.19 以上/22.7 以上の自動検出) | ESM |
上記に当てはまらない .js / .cjs | CommonJS(exports が使える) |
3行目の自動検出に注意してください。ただし、この記事の典型原因は次に述べる TypeScript の CommonJS 出力(exports.x = ...)と "type": "module" の食い違いです。tsc の CJS 出力は import/export を含まないため自動検出では ESM 化せず、あくまで "type": "module"(や .mjs)で ESM 扱いになったところに CJS 形のコードが載って落ちます。export を含む手書きファイルでは、自動検出でも同じ状況が起こり得ます。
いちばん多い原因:TypeScript の出力設定と type: module の食い違い
手書きで exports.x = ... と書くことは少なく、実際に多いのは TypeScript の設定の食い違いです。
tsconfig.jsonの"module": "commonjs"(既定に近い)で出力すると、tsc はexports.greeting = ...;やObject.defineProperty(exports, "__esModule", ...)という CommonJS 形の.jsを生成します。- 一方で
package.jsonに"type": "module"があると、その.jsは ESM として実行されます。 - 結果、CommonJS 形のコードが ESM として走り、
exports is not definedになります。
TypeScript のソース(.ts)では export const x = ... と ESM 構文で書いているのに、出力が CommonJS 形になっているのがポイントです。手元では出ず、type: "module" を入れた/ビルド設定を変えたタイミングで出る、という形になります。
再現(最小構成)
package.json を ESM 指定にします。
{
"type": "module"
}
同じフォルダの .js で exports を使います(tsc の CJS 出力を手で書いたもの)。
// index.js(type:module 下なので ESM 扱い)
exports.greeting = 'hello';
console.log(exports.greeting);
node index.js を実行すると ReferenceError: exports is not defined in ES module scope が出て、終了コードは 1 になります。
解決1:ESM の export 構文にする(手書きコードのとき)
自分で書いたコードなら、CommonJS の exports.x = ... を ESM の export に置き換えます。
// index.js(ESM のまま。export 構文に置き換えた)
export const greeting = 'hello';
console.log(greeting);
module.exports = foo; は export default foo; に対応します。これで終了コード 0 で動きます。
解決2:TypeScript の出力を ESM に合わせる(生成コードのとき)
exports.x = ... が tsc の出力なら、直すのはコンパイル設定です。package.json が "type": "module" なら、tsconfig.json を ESM 出力にそろえます。
{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
nodenext は、package.json の type を見て「CJS か ESM か」を判断し、ESM プロジェクトなら export 構文の .js を出力します。これで出力と実行時の扱いが一致します。逆に、プロジェクトを CommonJS のままにしたいなら、package.json の "type": "module" を外す(または対象を .cjs にする)方向でそろえます。tsc が出力する形式(module)と、package.json の type が決める実行時の扱いが一致していれば、この ReferenceError は出ません。
仕組み:exports は言語機能ではない
exports / module は言語機能ではなく、CommonJS がモジュールを実行するときに関数の引数として渡していた変数です(require / __dirname / __filename も同じ)。ESM はこの仕組みを使わないため、これらの変数はそもそも定義されません。ESM でモジュールの公開値を定義するのは export 文(言語機能)で、exports オブジェクトへの代入ではありません。
切り分け(うまくいかないとき)
exportsと一緒にrequire is not definedや__dirname is not definedも出る:どれも同じ原因(ESM 扱い)です。requireは require is not defined in ES module scope、__dirnameは __dirname is not defined in ES module scope を参照してください。Object.defineProperty(exports, "__esModule", ...)の行で落ちる:これは tsc の CommonJS 出力の先頭です。原因は本記事の「TypeScript の出力設定の食い違い」なので、解決2(tsconfig を ESM 出力に)で直します。- CommonJS のままにしたい:
package.jsonの"type": "module"を外すか、対象ファイルを.cjsにリネームすれば、exportsがそのまま使えます。type: "module"はそのpackage.json配下のサブフォルダも含めた.jsすべてを ESM にする点に注意してください。 - サードパーティの
.jsで出た:node_modules内のパッケージが CommonJS なのに ESM 扱いされている場合、そのパッケージのpackage.jsonや読み込み方の問題です。原因が別なので、この記事の対処では直りません。