Error [module is not defined in ES module scope]

module is not defined in ES module scope の直し方(Node.js)

FIX SUMMARY verified
Applies when
node:20-alpinemodule.exports in an ESM file

Verified: reproduced in node:20-alpine, then the module is not defined in ES module scope signature was gone after the fix (exit 0).

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" がある .jsESM
拡張子が .mjsESM(常に)
種別未確定の .jsimportexport などの ESM 構文がある(Node 20.19 以上/22.7 以上の自動検出)ESM
上記に当てはまらない .js / .cjsCommonJS(module.exports が使える)

表の3行目(自動検出)は、importexport などの ESM 構文があるファイルだけが対象です。module.exports だけで ESM 構文を含まないファイルは、type 指定が無ければ自動検出では ESM になりません。このエラーが自動検出で出るのは、同じファイルに importexportmodule.exports を混在させたときです。

ESM スコープには、CommonJS が用意していた module / exports / require / __dirname / __filename存在しません。だから「TypeScript や別プロジェクトの設定に合わせて type: "module" を入れた」「拡張子を .mjs にした」タイミングで、それまで動いていた module.exports がエラーになります。手元の古い設定では動いていたコードが、type: "module" を入れた環境(または上記の混在で自動検出された環境)でだけ落ちる、という形です。

再現(最小構成)

package.json を ESM 指定にします。

{
  "type": "module"
}

同じフォルダの .jsmodule.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.exportsexport に置き換える(基本)

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.jsonmodule を 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 を参照してください。

検証(machine-verified)

この修正は node:20-alpine のバージョン固定コンテナ内で再現し、修正後に module is not defined in ES module scope のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/module-not-defined-esm
● reproduce module is not defined in ES module scope present ✓
● apply fix exit 0
● re-run module is not defined in ES module scope gone ✓
PASS verified · node:20-alpine · signature gone