Node.js における CommonJS から ESM への移行:課題、進捗、およびベストプラクティス
By hungpd, at: 2025年8月21日15:29
Estimated Reading Time: __READING_TIME__ minutes


1. はじめに:モジュール戦争
10年以上にわたり、Node.js 開発者はCommonJSの世界で生きてきました。すべての require('fs')
と module.exports = foo
がバックエンドJavaScriptのDNAを定義していました。しかし2015年、ECMAScript委員会は、ブラウザとランタイムの両方に普遍的な標準を設定する、importとexportを備えたESモジュール(ESM)を導入しました。
Node.js 24 に早送り:ESMは単なる実験ではなく、デフォルトの方向性です。Next.js、Remix、Astroのようなフレームワークはすでに「ESMファースト」であり、パッケージメンテナーはメンテナンスコストを削減するためにCommonJSサポートをドロップしています。
しかし…多くの開発者はまだ不平を言っています。なぜでしょうか?npm(200万以上のパッケージ)ほど巨大なエコシステムを移行することは、貨物船を旋回させるようなもので、遅く、痛みを伴い、エッジケースに満ちているからです。
2. なぜこの移行は痛みを伴うのか
構文の違い
-
CommonJS:
const fs = require('fs');
module.exports = function hello() { return 'world'; }
-
ESM:
import fs from 'node:fs';
export default function hello() { return 'world'; }
これは化粧品のように見えるかもしれませんが、その意味合い(静的対動的解決、ホイスティング、非同期インポート)は深遠です。
ファイル拡張子と「type」:「module」の混乱
-
.cjs → 常にCommonJS
-
.mjs → 常にESM
-
.js → package.json の「type」フィールドに依存
これは数え切れないほどのチームを混乱させ、誤ったモードが推測されたために1つのファイルが突然壊れました。
エコシステムラグ
-
一部の重要なライブラリ(例:古いexpressミドルウェア)は、最近までCJSのみでした。
-
多くの開発者は、Jest、Webpack、またはMochaとのツール問題回避のためにCJSに固執しています。
3. 最近のNode.jsの改善(v22 → v24)
-
相互運用性のアップグレード:
Node 22 は、CommonJSからESMモジュールグラフ全体をrequire()する機能を紹介し、最大のブロッカーの1つを削除しました。
-
より良いエラーメッセージ:
「
ERR_REQUIRE_ESM
」のような暗号的なメッセージの代わりに、Nodeはモジュールがインポートできない理由を伝え、修正を提案します。
-
インポート属性:
Node 24(V8 13.6)は、インポートアサーションをネイティブでサポートしています:
import data from './data.json' assert { type: 'json' };
-
ローダーや実験的なフラグは不要です。
-
フレームワークの圧力:
Next.js などは、現在ESMビルドのみを提供しています。それに乗っていないと、取り残されます。
4. 実践的な移行:シナリオ
4.1 小規模プロジェクトの移行
-
package.json に「type」:「module」を追加します。
-
インポートを更新します:
-
require('foo') → import foo from 'foo'
-
module.exports = → export default
-
移行前(CJS):
const moment = require('moment');
module.exports = () => moment().format();
移行後(ESM):
import moment from 'moment';
export default () => moment().format();
4.2 両方の世界をサポートする(ライブラリ作成者)
条件付きエクスポートを使用します:
{
"exports": {
"import": "./esm/index.js",
"require": "./cjs/index.cjs"
}
}
これにより、ESMユーザーは最新のコードを取得し、レガシーアプリはCJSで実行され続けます。
4.3 レガシー依存関係
まだESMをサポートしていないパッケージについては、NodeはcreateRequireを提供します:
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacy = require('old-package');
5. 利点と欠点
側面 | ESMの利点 | 課題 / トレードオフ |
---|---|---|
標準化 | ブラウザとNodeで同じモジュールシステム | デュアルサポートはライブラリの複雑さを増す |
パフォーマンス | 静的分析によりツリーシェーキングと高速ビルドが可能 | 一部のケース(非同期インポート)でのコールドスタートペナルティ |
DX | よりクリーンな構文; 非同期import() |
.cjs 、.mjs 、および「type」に関する混乱 |
エコシステム | モダンなフレームワークはESMファースト | レガシーパッケージは移行しない可能性 |
6. ベストプラクティス
-
新規プロジェクト → 常にESM(「type」:「module」)。
-
ライブラリ → デュアルエクスポート CJSがさらにフェードアウトするまで。
-
アプリ → 徐々に移行:コアエントリポイントではなく、リーフモジュールから開始します。
-
ツールチェック:バンドラー/テストフレームワークが完全なESMサポートを持っていることを確認します。
-
チームの教育:import.meta.urlやcreateRequireのようなパターンを文書化します。
7. 結論
CommonJSからESMへの移行は、技術的なものよりも文化的なものです。多くの開発者は「移行疲れ」を感じていますが、2025年にESMを拒否することは2010年にGitの使用を拒否するようなもので、取り残されるでしょう。
Node.jsチームはその役割を果たしました:相互運用性の向上、フラグの削減、エラーの明確化。今度は開発者とメンテナーが標準を受け入れる番です。
テイクアウェイ:小さく始め、徐々に移行することで、よりクリーンで、より高速で、より将来性のあるコードベースのロックを解除できます。