Node.js における CommonJS から ESM への移行:課題、進捗、およびベストプラクティス

By hungpd, at: 2025年8月21日15:29

Estimated Reading Time: __READING_TIME__ minutes

Navigating the CommonJS-to-ESM Transition in Node.js: Pain Points, Progress, and Best Practices
Navigating the CommonJS-to-ESM Transition in Node.js: Pain Points, Progress, and Best Practices

1. はじめに:モジュール戦争

 

10年以上にわたり、Node.js 開発者はCommonJSの世界で生きてきました。すべての require('fs')module.exports = foo がバックエンドJavaScriptのDNAを定義していました。しかし2015年、ECMAScript委員会は、ブラウザとランタイムの両方に普遍的な標準を設定する、importとexportを備えたESモジュール(ESM)を導入しました。

 

Node.js 24 に早送り:ESMは単なる実験ではなく、デフォルトの方向性です。Next.jsRemixAstroのようなフレームワークはすでに「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チームはその役割を果たしました:相互運用性の向上、フラグの削減、エラーの明確化。今度は開発者とメンテナーが標準を受け入れる番です。

 

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

Tag list:

Subscribe

Subscribe to our newsletter and never miss out lastest news.