Node で .js を実行したら、module.exports の行で次のエラーが出て落ちることがあります。
ReferenceError: module 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 の module.exports でモジュールを公開しようとしているのが原因です。require が読み込み側で落ちるのと対になっていて、こちらは書き出し(export)側で落ちます。コードは変えていないのに急に出たなら、よくあるのは直前に package.json へ "type": "module" を足したか、ファイルを .mjs にしたケースです。
なぜ「急に」出るのか(ESM 扱いになる条件)
同じ module.exports のコードでも、そのファイルが CommonJS 扱いか ESM 扱いかで結果が変わります。次のどれかに当てはまるとファイルは ESM になり、module が未定義になります。
| 条件 | ファイルの扱い |
|---|---|
近い package.json に "type": "module" がある .js | ESM |
拡張子が .mjs | ESM(常に) |
種別未確定の .js に import/export などの ESM 構文がある(Node 20.19 以上/22.7 以上の自動検出) | ESM |
上記に当てはまらない .js / .cjs | CommonJS(module.exports が使える) |
表の3行目(自動検出)は、import/export などの ESM 構文があるファイルだけが対象です。module.exports だけで ESM 構文を含まないファイルは、type 指定が無ければ自動検出では ESM になりません。このエラーが自動検出で出るのは、同じファイルに import/export と module.exports を混在させたときです。
ESM スコープには、CommonJS が用意していた module / exports / require / __dirname / __filename が存在しません。だから「TypeScript や別プロジェクトの設定に合わせて type: "module" を入れた」「拡張子を .mjs にした」タイミングで、それまで動いていた module.exports がエラーになります。手元の古い設定では動いていたコードが、type: "module" を入れた環境(または上記の混在で自動検出された環境)でだけ落ちる、という形です。
再現(最小構成)
package.json を ESM 指定にします。
{
"type": "module"
}
同じフォルダの .js で module.exports を使います。
// index.js(type:module 下なので ESM 扱い)
module.exports = {
greet: (name) => `hello ${name}`,
};
node index.js を実行すると ReferenceError: module is not defined in ES module scope が出て、終了コードは 1 になります。
解決1:module.exports を export に置き換える(基本)
ESM では module.exports の代わりに export 文でモジュールの中身を公開します。名前を付けて公開するなら export、1つの値を「これがこのモジュールの中身」として公開するなら export default です。
// index.js(ESM のまま。module.exports を export に置き換えた)
export const greet = (name) => `hello ${name}`;
console.log(greet('errfix'));
これで終了コード 0 で動きます。読み込む側も、const { greet } = require('./index.js') ではなく import { greet } from './index.js' のように import 文で受け取ります。module.exports = 関数やオブジェクト1つ(CommonJS の「まるごと1つを公開」)に相当するのは export default です。
// まるごと1つを公開したいとき
export default {
greet: (name) => `hello ${name}`,
};
export default にした場合、読み込む側は import config from './index.js' のように既定 import で受けます。import { greet } from './index.js' の名前付きでは受け取れず、does not provide an export named という別のエラーになります。CommonJS のオブジェクト公開をそのまま移すなら、個々を export const で名前付きにするほうが、読み込み側も名前付き import で受けられて素直です。
解決2:その1ファイルだけ CommonJS に戻したいとき(.cjs)
移行の途中で、module.exports を使う既存ファイルをそのまま残したいこともあります。その場合は、そのファイルだけ拡張子を .cjs にすると、package.json の "type": "module" の影響を受けずに CommonJS 扱いに戻り、module.exports がそのまま使えます。
utils.js → utils.cjs
ただし、.cjs を ESM から読み込むときは、名前付き import が通らないことがあります。Node は CommonJS 側のコードを静的に解析して名前を拾えたときだけ名前付きを許すので、確実なのは import utils from './utils.cjs' のように既定 import で受けて utils.greet のように使う形です。名前付きで落ちたときの詳細は does not provide an export named / Named export not found を参照してください。プロジェクト全体を ESM に寄せているなら、.cjs で残すのは橋渡しと考え、最終的には export へそろえるほうが、依存関係を静的にたどれてバンドラや型チェックの解析対象になります。
仕組み:module は CommonJS が渡していた引数
module は言語機能ではなく、CommonJS がモジュールを実行するときに関数の引数として渡していた変数です(exports / require / __dirname / __filename も同じ)。CommonJS は各モジュールを (function (exports, require, module, __filename, __dirname) { ... }) のような関数で包んで実行するので、その中では module が使えました。ESM はこの仕組みを使わないため、これらの変数はそもそも定義されません。だから ESM 扱いのファイルで module.exports を書くと「未定義の変数を参照した」= ReferenceError になります。
公開の手段は、ESM では言語機能の export / export default です。ESM のファイルではこちらを使います。
切り分け(うまくいかないとき)
moduleと一緒にexports is not definedも出る(TypeScript でよくある):tscが CommonJS 形式(Object.defineProperty(exports, ...)やmodule.exports)を出力しているのに、実行環境が ESM だと起きます。原因も対処も同型で、exportsの側は exports is not defined in ES module scope を参照してください。多くはtsconfig.jsonのmoduleを ESM 系(nodenextなど)にそろえるのが根本対処です。moduleと一緒に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 を参照してください。- 本当は ESM にしたくなかった:
package.jsonの"type": "module"を外すか、そのファイルを.cjsにリネームすれば CommonJS 扱いに戻り、module.exportsがそのまま使えます。逆に ESM に寄せる場合、type: "module"は**より近いpackage.jsonで上書きされない範囲の.js**を ESM にする点に注意してください(途中のサブパッケージに"type": "commonjs"があれば、そこから下は CommonJS に戻ります)。 - そもそも
import文の側でCannot use import statement outside a moduleが出る:症状が反対(CJS 扱いなのに ESM 構文を書いた)のケースです。対処は Cannot use import statement outside a module を参照してください。