Error [require is not defined in ES module scope]

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

FIX SUMMARY verified
Applies when
node:20-alpinerequire() in an ESM file

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

Node で .js を実行したら、次のエラーで落ちることがあります。

ReferenceError: require is not defined in ES module scope, you can use import instead

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 の require() を使っているのが原因です。コードは変えていないのに急に出たなら、よくあるのは直前に package.json"type": "module" を足したか、ファイルを .mjs にしたケースです(新しい Node では、後述の自動検出で ESM 扱いになることもあります)。

なぜ「急に」出るのか(ESM 扱いになる条件)

同じ require() のコードでも、そのファイルが CommonJS 扱いか ESM 扱いかで結果が変わります。次のどれかに当てはまるとファイルは ESM になり、require が使えなくなります。

条件ファイルの扱い
近い package.json"type": "module" がある .jsESM
拡張子が .mjsESM(常に)
種別未確定の .jsimportexport などの ESM 構文がある(Node 20.19 以上/22.7 以上の自動検出)ESM
上記に当てはまらない .js / .cjsCommonJS(require が使える)

3行目に注意してください。import 文と require()混在した .js を新しい Node で実行すると、自動検出でファイル全体が ESM 扱いになり、require の側が落ちます(type: "module".mjs も無いのに出るのはこのパターンです。詳細は Cannot use import statement outside a module の自動検出の項)。

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

再現(最小構成)

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

{
  "type": "module"
}

同じフォルダの .jsrequire() を使います。

// index.js(type:module 下なので ESM 扱い)
const os = require('os');
console.log(os.platform());

node index.js を実行すると ReferenceError: require is not defined in ES module scope が出て、終了コードは 1 になります。

解決1:requireimport に置き換える(基本)

ESM ではファイル先頭の import 文でモジュールを読み込みます。組み込みモジュールは node: プレフィックスを付けると、同名のユーザーパッケージに解決を奪われず、必ず組み込みモジュールへ解決されます。

// index.js(ESM のまま。require を import 文に置き換えた)
import os from 'node:os';
console.log(os.platform());

これで終了コード 0 で動きます。自分のモジュールも import { foo } from './foo.js'; のように、拡張子まで書いて読み込みます。

解決2:どうしても require を残したいとき(createRequire

CommonJS 専用のパッケージや、require('./data.json') のような読み込みを残したい場合は、node:modulecreateRequire で ESM の中に require を作れます。

// index.js(ESM のまま require を復活させる)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

const os = require('os');
console.log(os.platform());

import.meta.url を基準に解決するので、相対パスの require('./x') もそのまま使えます。ただしこれは移行の橋渡しです。恒久的には import に統一したほうが、依存関係を静的にたどれ、バンドラや型チェックの解析対象になります。

仕組み:ESM スコープに CommonJS の変数は無い

require は言語機能ではなく、CommonJS がモジュールを実行するときに関数の引数として渡していた変数です(module / exports / __dirname / __filename も同じ)。ESM はこの仕組みを使わないため、これらの変数はそもそも定義されません。だから ESM 扱いのファイルで require を書くと「未定義の変数を参照した」= ReferenceError になります。

import は ESM の言語機能なので、ESM のファイルではこちらを使います。

切り分け(うまくいかないとき)

  • require と一緒に __dirname is not definedmodule is not defined も出る:どれも同じ原因(ESM 扱い)です。__dirname の代わりは、ローカルの file: ESM なら import.meta.dirname(Node 20.11 / 21.2 以上)、それ以前や file: 以外では import.meta.url から fileURLToPath で求めます。
  • 本当は ESM にしたくなかったpackage.json"type": "module" を外すか、そのファイルだけ .cjs にリネームすれば CommonJS 扱いに戻り、require がそのまま使えます。逆に ESM に寄せる場合、type: "module"その package.json 配下のサブフォルダも含めた .js すべてを ESM にする点に注意してください。
  • import に変えたら別のエラーになった:CommonJS パッケージから名前付き import しようとすると失敗することがあります(既定エクスポートしか無いため)。対処は does not provide an export named / Named export not found を参照してください。
  • 逆に、CommonJS 側から ESM を require() して落ちた:症状が反対(CJS のまま ESM を読みたい)のケースです。対処は require() of ES Module not supported(ERR_REQUIRE_ESM) を参照してください。

検証(machine-verified)

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

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