【実務・中級編】Node.js 22以降の重要変更点:require(esm)の現状と最新のモジュール解決戦略 – 実行環境・ランタイム・コンパイラ生産性向上バイブル

Node.js 22の革命:`require(esm)`の封印解除と、モダンJSプロジェクトの「終わらない移行」を終わらせる戦略

こんにちは。テックリードです。

Node.jsのエコシステムにおいて、CommonJS (CJS) と ES Modules (ESM) の分断は、ここ数年、開発者の貴重な思考時間を奪い続けてきた最大の「負債」でした。しかし、Node.js 22における `require()` でのESM同期読み込み対応は、この悪夢に終止符を打つための重要な布石です。

本稿では、単なる新機能の紹介ではなく、なぜ今、この変更が「歴史的な転換点」なのか、そして現場でこの技術をどう使いこなし、CI/CDやチーム開発の生産性を最大化すべきかを、アーキテクトの視点から深掘りします。

—

1. なぜ `require(esm)` が「禁断の果実」だったのか

これまで、Node.jsにおいてESMは「非同期」であることが原則でした。これは、ESMの仕様自体がトップレベルの非同期評価を前提としているからです。一方、`require` は同期的な解決を行うため、両者は本質的に相容れない存在でした。

Node.js 22で導入された `–experimental-require-module` は、「同期的に解決可能なモジュールグラフ」に限定して、CJSからESMを直接読み込めるようにするという、極めて実用的な妥協案です。

現場でこの機能がもたらすもの

  • 移行の段階的緩和: 全ファイルを一気に `.mjs` に変える必要はもうありません。古いCJSベースのテストスイートや設定ファイルから、最新のESMパッケージを「必要な部分だけ」同期的に呼び出せます。
  • メタプログラミングの復権: WebpackのプラグインやJestのカスタマイズなど、CJS前提で動くツールチェーンにESMを組み込む際、`import()` のトップレベルawaitで苦しんでいたコードが、劇的にシンプルになります。

—

2. 実践:`require(esm)` を安全に統合する設定戦略

この機能を有効にするには、実行時にフラグを付与するだけですが、チーム全体でこの設定を共有化し、一貫性を保つには `package.json` の `scripts` や `node` の設定ファイルを活用するのが正攻法です。

ベストプラクティス:`.node-options` を活用した環境同期

環境変数やCLI引数に依存すると、開発者の環境差分でバグが混入します。リポジトリルートに `.node-options` を配置し、Node.jsの起動オプションをプロジェクト単位で固定しましょう。

.node-options (ルート直下に配置)
–experimental-require-module
–no-warnings
警告を抑制し、ESM読み込みの実験的機能をデフォルトで有効化する

—

3. チーム開発の生産性を最大化する「神プラグイン」と設定

開発環境において、モジュール解決の「迷子」は生産性を著しく下げます。以下の構成を導入し、IDEレベルで解決しましょう。

推奨プラグイン:ESLint & TypeScript の「Resolver」

VS Codeを使っているなら、`ESLint` と `TypeScript` の設定で、CJSとESMの共存を明示的にサポートさせる必要があります。

`eslint.config.mjs` (最新のFlat Config形式)

import nodePlugin from ‘eslint-plugin-n’;

export default [
{
plugins: { n: nodePlugin },
rules: {
// CJS/ESMの混在環境でのモジュール解決を静的解析で守る
‘n/no-missing-import’: ‘error’,
‘n/no-unsupported-features/es-syntax’: ‘off’,
},
},
];

隠れた神ショートカット:VS Codeの「Go to Definition」

`package.json` の `”exports”` フィールドが複雑化すると、どのファイルが読み込まれているのか把握できなくなります。

  • `Cmd + Click` (Win: `Ctrl + Click`): これを使い、「どのファイルが解決されているか」を常に意識する癖をつけてください。
  • VS Code Command Palette: `TypeScript: Go to Project Configuration` を使い、`tsconfig.json` の `moduleResolution: “bundler”` が正しく設定されているか、常にチェックするルーチンを組み込みます。

—

4. アーキテクトが教える「移行のロードマップ」

いきなり全てをESMにするのは愚策です。以下のステップで進めてください。

1. Phase 1: 依存関係の断捨離: `dependencies` にある古いパッケージを最新版へ更新。`type: module` に対応しているか調査。
2. Phase 2: `–experimental-require-module` の導入: プロジェクトの主要なエントリーポイントはCJSのまま、テストやユーティリティスクリプトからESMを読み込み始める。
3. Phase 3: `exports` フィールドの整備: `package.json` に `exports` を記述し、CJSとESMのデュアルパッケージング(条件付きエクスポート)を完成させる。

現場で役立つ `package.json` のエクスポート構成例

{
“name”: “my-project”,
“exports”: {
“.”: {
“import”: “./dist/index.mjs”,
“require”: “./dist/index.cjs”
}
}
}

このように定義することで、ライブラリ利用者側が `import` を使えばESMが、`require` を使えばCJSが自動的に選択されます。これが「モダンなNode.jsライブラリ開発」の最低ラインです。

—

最後に:ツールを使いこなすということ

Node.js 22の `require(esm)` は、単なる機能追加ではありません。それは、私たちが「過去の遺産(CJS)」を捨てずに、「未来の標準(ESM)」へ舵を切るための架け橋です。

アーキテクトとして皆さんに伝えたいのは、「ツールに振り回されるのではなく、ツールの進化を先回りして、プロジェクトの『負債の利子』を最小化せよ」ということです。

今日紹介した設定を、今すぐあなたのプロジェクトの `.node-options` に書き込んでみてください。その瞬間から、ESMとCJSの壁に悩まされる時間は、確実にゼロに近づきます。

開発環境は、常に「摩擦ゼロ」を目指す。それが最強のチームを作る唯一の道です。

タイトルとURLをコピーしました