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 小規模プロジェクトの移行

  1. package.json に「type」:「module」を追加します。
     

  2. インポートを更新します:

    • 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. ベストプラクティス

  1. 新規プロジェクト → 常にESM(「type」:「module」)。
     

  2. ライブラリ → デュアルエクスポート CJSがさらにフェードアウトするまで。
     

  3. アプリ → 徐々に移行:コアエントリポイントではなく、リーフモジュールから開始します。
     

  4. ツールチェック:バンドラー/テストフレームワークが完全なESMサポートを持っていることを確認します。
     

  5. チームの教育:import.meta.urlやcreateRequireのようなパターンを文書化します。

7. 結論

CommonJSからESMへの移行は、技術的なものよりも文化的なものです。多くの開発者は「移行疲れ」を感じていますが、2025年にESMを拒否することは2010年にGitの使用を拒否するようなもので、取り残されるでしょう。

Node.jsチームはその役割を果たしました:相互運用性の向上、フラグの削減、エラーの明確化。今度は開発者とメンテナーが標準を受け入れる番です。

テイクアウェイ:小さく始め、徐々に移行することで、よりクリーンで、より高速で、より将来性のあるコードベースのロックを解除できます。