ある日、npmパッケージをアップデートしたら突然こんなエラーが出た——という経験はないだろうか。
Error [ERR_REQUIRE_ESM]: require() of ES Module
/path/to/node_modules/chalk/source/index.js not supported.
Instead change the require of index.js to a dynamic import()
which is available in all CommonJS modules.
chalk v5・node-fetch v3・p-limit v4・globby v13 など、多くの人気パッケージが ESM(ES Modules)専用になったことで、CommonJS(CJS)プロジェクトでそのまま使えなくなった。この記事では根本原因と、状況に応じた5つの対処法を解説する。
ERR_REQUIRE_ESM が出るパッケージ一覧(主要なもの)
| パッケージ | ESM専用になったバージョン |
|---|---|
| chalk | v5.0.0 以降 |
| node-fetch | v3.0.0 以降 |
| p-limit | v4.0.0 以降 |
| globby | v13.0.0 以降 |
| execa | v6.0.0 以降 |
| got | v12.0.0 以降 |
| ora | v6.0.0 以降 |
| nanoid | v4.0.0 以降 |
| find-up | v6.0.0 以降 |
| is-stream | v3.0.0 以降 |
package.json に "type": "module" が記載されているか、exports フィールドで import 条件のみ提供されているパッケージは ESM 専用だ。
なぜ ERR_REQUIRE_ESM が起きるのか
Node.js のモジュールシステムには2種類ある。
CommonJS(CJS)
require()/module.exportsを使う旧来の形式package.jsonに"type"指定がない場合、.jsはデフォルトで CJS
ES Modules(ESM)
import/exportを使うモダンな形式package.jsonに"type": "module"が必要、または.mjs拡張子を使う
問題の核心:require() は同期的に動作するため、非同期で解決される ESM モジュールを読み込めない。これは Node.js の仕様上の制約であり、設定で回避できるものではない。
対処法1:パッケージをバージョンダウンする(応急処置)
ESM 専用になる前のバージョンに固定する。最も手軽だが一時しのぎにすぎない。
# chalk の場合(v4 は CJS 対応)
npm install chalk@4
# node-fetch の場合(v2 は CJS 対応)
npm install node-fetch@2
# p-limit の場合(v3 は CJS 対応)
npm install p-limit@3
// chalk v4 は CJS で問題なく使える
const chalk = require('chalk')
console.log(chalk.blue('Hello, world!'))
デメリット:セキュリティパッチ・新機能が受け取れない。根本解決にならない。
対処法2:dynamic import() を使う(推奨)
require() の代わりに import() を使う。import() は CJS ファイルからでも呼べるため、ファイル形式を変えずに ESM パッケージを利用できる。
// Before(エラーになる)
const chalk = require('chalk')
// After(動く)
const { default: chalk } = await import('chalk')
console.log(chalk.blue('Hello!'))
await が必要なため、非同期関数の中で使う必要がある。トップレベルの await は ESM ファイルでのみ使用可能だ。
// CJS ファイル内での実用例
async function main() {
const { default: chalk } = await import('chalk')
const { default: fetch } = await import('node-fetch')
console.log(chalk.green('Starting...'))
const res = await fetch('https://api.example.com/data')
const data = await res.json()
console.log(data)
}
main().catch(console.error)
デメリット:関数内でしか使えず、モジュールスコープでの定数として扱いにくい。
対処法3:ファイルを .mjs に変更する(部分的 ESM 化)
ESM パッケージを使いたいファイルだけ .mjs 拡張子に変更する。Node.js は .mjs を常に ESM として扱う。
mv src/utils/mailer.js src/utils/mailer.mjs
// mailer.mjs — ESM 形式で書ける
import chalk from 'chalk'
import fetch from 'node-fetch'
export async function sendMail(to, subject, body) {
console.log(chalk.green(`Sending mail to ${to}...`))
// ...
}
CJS ファイルから .mjs を呼ぶ場合は import() が必要になる:
// main.js(CJS)から .mjs ファイルを使う
async function run() {
const { sendMail } = await import('./utils/mailer.mjs')
await sendMail('user@example.com', 'Hello', 'Body text')
}
run()
デメリット:CJS と ESM が混在してコードベースが複雑になりやすい。
対処法4:プロジェクト全体を ESM 化する(本命)
package.json に "type": "module" を追加し、プロジェクト全体を ESM に移行する。新規プロジェクトや比較的小さなコードベースに向いている。
{
"name": "my-project",
"version": "1.0.0",
"type": "module"
}
全ての .js ファイルを ESM 形式に書き換える:
// Before(CJS)
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
function greet(name) {
console.log(chalk.blue(`Hello, ${name}!`))
}
module.exports = { greet }
// After(ESM)
import fs from 'fs'
import path from 'path'
import chalk from 'chalk'
export function greet(name) {
console.log(chalk.blue(`Hello, ${name}!`))
}
ESM 移行時の注意:__dirname と __filename が使えない
ESM では __dirname と __filename がデフォルトで使えない。次の代替コードを使う:
// ESM での __dirname / __filename の代替
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// Node.js 21.2.0 以降はこれだけでOK
// const __dirname = import.meta.dirname
ESM 移行時の注意:Jest が動かなくなる
ESM 化後に Jest を使う場合、追加設定または実験的フラグが必要になる:
# Jest を ESM モードで実行
node --experimental-vm-modules node_modules/.bin/jest
これが面倒な場合は Vitest への移行を検討しよう。Vitest は Vite ベースで ESM をネイティブサポートしており、Jest と API 互換性が高い。
対処法5:TypeScript + ts-node を使っている場合
TypeScript プロジェクトで ts-node を使っている場合、ESM 化には追加設定が必要だ。
// tsconfig.json
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
// package.json
{
"type": "module",
"scripts": {
"dev": "node --loader ts-node/esm src/index.ts"
}
}
ただし、設定が煩雑になりがちだ。tsx を使うほうがシンプルでトラブルも少ない:
npm install -D tsx
{
"scripts": {
"dev": "tsx src/index.ts",
"start": "tsx src/index.ts"
}
}
tsx は CJS・ESM を自動判別するため、package.json の "type" 設定にかかわらず動作する。設定を最小限に抑えたいプロジェクトに最適だ。
どの対処法を選ぶべきか
状況ごとの判断基準をまとめる。
| 状況 | 推奨対処法 |
|---|---|
| 新規プロジェクト | 対処法4(最初から ESM で始める) |
| 小〜中規模の既存プロジェクト | 対処法4(ESM 化に踏み切る) |
| 大規模・レガシーな既存プロジェクト | 対処法2(dynamic import で段階的に対応) |
| 急いでいる・一時的な対応 | 対処法1(バージョンダウン) |
| TypeScript + ts-node | 対処法5(tsx への移行を検討) |
| 特定ファイルだけ ESM パッケージを使いたい | 対処法3(.mjs ファイル化) |
よくあるつまずきポイント
“type”: “module” にしたら既存コードが一斉に壊れた
ESM 化後は require() が一切使えなくなる。全ての require() を import に書き換える必要がある。一気に移行が難しい場合は、対処法2(dynamic import)で段階的に対応するか、ファイル単位で .mjs / .cjs 拡張子を使って混在させる方法もある。
CJS のパッケージを ESM から使う場合は問題ない
逆パターン(ESM プロジェクトから CJS パッケージを import)は Node.js が自動変換するため問題なく動く。エラーが起きるのは CJS から ESM を require() する方向だけだ。
require.resolve() も同様にエラーになる
require() だけでなく require.resolve() も ESM パッケージには使えない。パスの解決には import.meta.resolve()(Node.js 20.6.0 以降)を使う:
// ESM でのパス解決
const resolvedPath = import.meta.resolve('chalk')
console.log(resolvedPath) // file:///path/to/node_modules/chalk/source/index.js
まとめ
ERR_REQUIRE_ESM は Node.js のモジュールシステム移行期に避けられないエラーだ。根本的な解決はプロジェクトの ESM 化だが、コードベースの規模や緊急度に応じて対処法を選ぶのが現実的だ。
| 原因 | 対策 |
|---|---|
| chalk v5+ など人気パッケージの ESM 専用化 | バージョンダウン or dynamic import |
require() は同期的で ESM を読めない |
import() は非同期で CJS からも使える |
| プロジェクト全体の移行が必要 | "type": "module" + 全ファイル書き換え |
| TypeScript + ts-node 環境 | tsx への移行が最もシンプル |
長期的には ESM への完全移行が正解だ。新規プロジェクトは最初から ESM で始め、既存プロジェクトは dynamic import を足がかりに段階的に移行していくのがよいだろう。
