Error [exports is not defined in ES module scope]

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

FIX SUMMARY verified
Applies when
node:20-alpineexports under type:module (tsc CJS output)

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

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

3行目の自動検出に注意してください。ただし、この記事の典型原因は次に述べる TypeScript の CommonJS 出力(exports.x = ...)と "type": "module" の食い違いです。tsc の CJS 出力は importexport を含まないため自動検出では 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" があると、その .jsESM として実行されます。
  • 結果、CommonJS 形のコードが ESM として走り、exports is not defined になります。

TypeScript のソース(.ts)では export const x = ... と ESM 構文で書いているのに、出力が CommonJS 形になっているのがポイントです。手元では出ず、type: "module" を入れた/ビルド設定を変えたタイミングで出る、という形になります。

再現(最小構成)

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

{
  "type": "module"
}

同じフォルダの .jsexports を使います(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.jsontype を見て「CJS か ESM か」を判断し、ESM プロジェクトなら export 構文の .js を出力します。これで出力と実行時の扱いが一致します。逆に、プロジェクトを CommonJS のままにしたいなら、package.json"type": "module" を外す(または対象を .cjs にする)方向でそろえます。tsc が出力する形式(module)と、package.jsontype が決める実行時の扱いが一致していれば、この ReferenceError は出ません。

仕組み:exports は言語機能ではない

exports / module は言語機能ではなく、CommonJS がモジュールを実行するときに関数の引数として渡していた変数です(require / __dirname / __filename も同じ)。ESM はこの仕組みを使わないため、これらの変数はそもそも定義されません。ESM でモジュールの公開値を定義するのは export 文(言語機能)で、exports オブジェクトへの代入ではありません。

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

  • exports と一緒に require is not defined__dirname is not defined も出る:どれも同じ原因(ESM 扱い)です。requirerequire 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 や読み込み方の問題です。原因が別なので、この記事の対処では直りません。

検証(machine-verified)

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

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