状況の説明
深夜2時。デプロイが失敗するか、依存関係のバージョンを上げた後にローカルビルドが壊れた。スタックトレースはこんな感じだ:
Error [ERR_REQUIRE_ESM]: require() of ES Module /path/to/node_modules/node-fetch/src/index.js not supported.
/path/to/node_modules/node-fetch/src/index.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package as ES modules.
Instead either rename /path/to/node_modules/node-fetch/src/index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /path/to/node_modules/node-fetch/src/index.js.
at Object. (/your/project/src/api.js:3:18)
コードは何も変えていない — パッケージのバージョンを変えただけだ。これがCommonJS対ESMの互換性の壁というやつだ。
実際に何が起きているのか
Node.jsは2つのモジュールシステムをサポートしている:
- CommonJS (CJS):
require()/module.exportsを使う — 従来のNode.js方式 - ESM (ES Modules):
import/exportを使う — モダンな標準
問題は、CJSはESMモジュールをrequire()できないということだ。これは絶対だ。2021年頃から、node-fetch v3、chalk v5、nanoid v4、got v12、execa v6などの人気パッケージがCJSサポートを完全に廃止し、ESM専用になった。これらのうち一つでもアップグレードした瞬間、require()を使っているプロジェクトは壁にぶつかる。
まず根本原因を特定する
修正方法を選ぶ前に、プロジェクトがどのモジュールシステムを使っているか確認しよう:
cat package.json | grep '"type"'
typeフィールドなし、または"type": "commonjs"→ プロジェクトはCJS"type": "module"→ プロジェクトはESM
次に問題のパッケージを確認する:
cat node_modules/node-fetch/package.json | grep '"type"'
"type": "module"と書いてあれば、そのパッケージはESM専用だ — require()はできない。これで問題の正体がわかった。以下の修正方法から選んでほしい。
クイックフィックス:最後のCJSバージョンに固定する
5分以内に動かしたい?パッケージを最後のCommonJS互換バージョンに固定しよう。コードの変更は不要だ。
# node-fetch: 最後のCJSバージョンはv2
npm install node-fetch@2
# chalk: 最後のCJSバージョンはv4
npm install chalk@4
# nanoid: 最後のCJSバージョンはv3
npm install nanoid@3
# got: 最後のCJSバージョンはv11
npm install got@11
# execa: 最後のCJSバージョンはv5
npm install execa@5
修正を確認する:
node -e "const fetch = require('node-fetch'); console.log(typeof fetch);"
functionと表示されるはずだ。ブロックが解消された。リリースしよう。
恒久的な修正オプション1:動的import()を使う
プロジェクト全体を変換せずに最新バージョンのパッケージを使いたい?動的import()が橋渡しになる。CJSファイルはimport()を呼び出せる — ファイルの先頭に静的なimportキーワードを使えないだけだ。
// 修正前(動かない)
const fetch = require('node-fetch');
// 修正後(CJSファイルで動作する)
async function fetchData(url) {
const { default: fetch } = await import('node-fetch');
const res = await fetch(url);
return res.json();
}
{ default: fetch }の分割代入に注目してほしい。CJSから動的importでESMのデフォルトエクスポートにアクセスする場合、値は直接ではなくdefaultプロパティに入る。
動作確認のクイックテスト:
node -e "
async function test() {
const { default: fetch } = await import('node-fetch');
const res = await fetch('https://httpbin.org/get');
console.log(res.status);
}
test();
"
恒久的な修正オプション2:プロジェクトをESMに変換する
長期的にクリーンな解決策を求めるなら、プロジェクト全体をESMに移行しよう。最初の作業は増えるが、互換性の問題を完全に解消できる。
ステップ1:package.jsonを更新する
{
"type": "module"
}
ステップ2:require/module.exportsをimport/exportに置き換える
// 修正前(CJS)
const express = require('express');
const { readFileSync } = require('fs');
module.exports = { myFunction };
// 修正後(ESM)
import express from 'express';
import { readFileSync } from 'fs';
export { myFunction };
ステップ3:__dirnameと__filenameを修正する(ESMでは存在しない)
// __dirnameが必要なファイルの先頭にこれを追加する
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
ステップ4:動的requireを修正する
// 修正前
const config = require(`./configs/${env}.js`);
// 修正後
const { default: config } = await import(`./configs/${env}.js`);
プロジェクトを実行して何が壊れるか確認しよう:
node src/index.js
さらにいくつかのエッジケースが出てくるはずだ — JSONインポート、require.resolve、ESMサポートのない古いパッケージなど。表面化したら順番に修正していこう。
恒久的な修正オプション3:バンドラーやトランスパイラーを使う
手動移行のリスクが高い?バンドラーにCJS/ESMの変換を任せよう。
# esbuild — 最速の選択肢
npm install -D esbuild
npx esbuild src/index.js --bundle --platform=node --outfile=dist/index.cjs
# tsx — TypeScriptプロジェクトに最適
npm install -D tsx
npx tsx src/index.ts
TypeScriptユーザーはESMスタイルのインポートを書きながら、CJS出力のままにできる:
// tsconfig.json
{
"compilerOptions": {
"module": "CommonJS",
"esModuleInterop": true
}
}
特殊ケース:Jestで実行する場合
JestはデフォルトでCJSモードで動くため、ESMパッケージはテストも壊す。2つの選択肢がある:
# オプション1:JestでESMサポートを有効にする
NODE_OPTIONS='--experimental-vm-modules' npx jest
# オプション2:JestにESMパッケージを変換するよう指示する
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(node-fetch|chalk|nanoid)/)',
],
};
オプション1のほうがセットアップが簡単だ。オプション2は複数のESM専用パッケージが関わる場合に細かい制御ができる。
動作確認
修正後にこの3つのチェックを実行しよう:
# 1. エントリーポイントを直接実行する
node src/index.js
# 2. テストスイートを実行する
npm test
# 3. 特定のインポートをスポットチェックする
node -e "import('node-fetch').then(m => console.log('OK:', typeof m.default))"
出力のどこにもERR_REQUIRE_ESMが出ない?完了だ。
どの修正を選ぶか
- 5分以内に直したい:最後のCJSバージョンに固定する
- 最新パッケージを使いたい、リファクタは最小限に:動的
import()を使う - 新規プロジェクト、またはリファクタできる:プロジェクトをESMに変換する
- TypeScriptプロジェクト:tsonfigで
"module": "CommonJS"とesModuleInterop: trueを設定する

