Node.jsのESM解決アルゴリズム(Resolution Algorithm)が担う役割は次の2つだけです。
import内のspecifierを最終的なURLへ解決する- そのURLに対応するモジュール形式を判定する
たとえば:
Node.jsが最終的に得る結果は次のとおりです。
react -> file:///project/node_modules/react/index.js
./utils.js -> file:///project/src/utils.js
#config -> file:///project/src/config/index.js
さらに:
module
commonjs
json
wasm
これらはNode.jsが認識するモジュール形式です。
ESM仕様は、本質的に次の流れを説明しています:
specifier
↓
resolved URL
↓
module format
↓
load
↓
execute
一、ESM_RESOLVEの全体フロー
Node.jsのESMで開始点となるアルゴリズム:
ESM_RESOLVE(specifier, parentURL)
これは次のように簡略化できます:
アルゴリズム全体の中心にある目標は次のとおりです:
specifier
↓
唯一 URL
CommonJSのように候補を延々と推測する方式ではありません。
二、specifierの分類
Node.jsはspecifierを4種類に分類します。
1. URL specifier
たとえば:
または:
specifier自体が有効なURLなら:
new URL(specifier)
成功します。
その結果をそのまま返します。
2. 相対specifier
たとえば:
Node.jsは現在のモジュールの位置を基準に解決します。
次のURLを前提とします:
file:///app/src/main.js
ここで:
解決結果は次のようになります:
file:///app/src/utils.js
実質的には次の処理です:
3. パッケージimports
たとえば:
または:
specifierが次の文字で始まる場合:
#
は、次の処理に入ります:
PACKAGE_IMPORTS_RESOLVE()
現在のパッケージのpackage.jsonを読み取ります:
たとえば:
最終的な解決結果は次のとおりです:
./src/db/index.js
4. bare specifier
たとえば:
次のいずれにも該当しないspecifierは、bare specifier(裸パッケージ名)です。
次の処理に入ります:
PACKAGE_RESOLVE()
node_modulesの探索が始まります。
三、PACKAGE_RESOLVE
Node.jsがnpmパッケージを探すときの中核となるロジックです。
たとえば:
現在のファイル:
/app/src/pages/home/index.js
Node.jsは親ディレクトリへ遡って次のような場所を探します:
/app/src/pages/home/node_modules/react
/app/src/pages/node_modules/react
/app/src/node_modules/react
/app/node_modules/react
/node_modules/react
見つかるまで探索を続けます。
概念的には次と同等です:
while(currentDirectory){
查找 node_modules/packageName
找不到继续向上
}
四、package.jsonの読み取り
パッケージが見つかったら:
node_modules/react/package.json
Node.jsが設定の読み込みを開始します。
優先順位は次のとおりです:
exports
↓
main
↓
直接路径
五、exportsの仕組み
現代のNode.jsでは、パッケージ解決のほぼ全体がexportsに依存しています。
たとえば:
次のimportが利用できます:
対応するエントリは:
.
解決結果は次のようになります:
./dist/index.js
次のimportも利用できます:
対応するエントリは:
./jsx-runtime
解決結果は次のようになります:
./dist/jsx-runtime.js
ただし:
exportsに次のエントリがなければ、
次のエラーになります:
Package Path Not Exported
六、exportsの本質
exportsは、パッケージが外部に公開するAPIの範囲を指します。次のように表せます:
包对外暴露的公开 API
たとえば:
次のimportが利用できます:
次のimportは禁止されます:
対象ファイルが実際に存在していても、importできません。
七、条件付きexports
exportsの値は文字列とは限りません。
オブジェクトにもできます。
たとえば:
Node.jsは条件に応じて値を選びます。
ESM:
一致する条件は:
解決結果は次のようになります:
./dist/index.mjs
CommonJS:
一致する条件は:
解決結果は次のようになります:
./dist/index.cjs
八、PACKAGESELFRESOLVE
次の構成を想定します:
現在のコードは次の場所にあります:
my-lib
その内部にある場合:
外部のnode_modulesは探索しません。
Node.jsは次のことを検出します:
当前包名就是 my-lib
現在のpackage.jsonのexportsを直接使います。
この処理は次のように呼ばれます:
PACKAGE_SELF_RESOLVE
九、importsの仕組み
importsはexportsとよく似ています。
exportsは他の利用者向け、importsはパッケージ内部での利用向けです。コードでは次のように表せます:
exports 给别人用
imports 给自己用
たとえば:
これにより:
実際に解決されるパスは次のようになります:
./src/db/index.js
もう1つ例を示します:
解決結果は次のとおりです:
./src/utils/string.js
十、パターンマッチ
importsとexportsはワイルドカードに対応しています。
たとえば:
次のimportを実行すると、
パターン内の
*
が入力に一致します。置換結果は
user
最終的なパスは次のようになります:
./src/features/user.js
十一、PACKAGETARGETRESOLVE
exports/importsのマッピングを実際に適用する処理です。
次の4種類の値を扱えます。
文字列
指定された文字列をそのまま解決します。
オブジェクト
条件(conditions)に応じて適切な値を選びます。
配列
候補は順番に調べられます。前の候補で失敗すると、
次の候補を試します。
null
そのパスからのエクスポートを明示的に禁止します。
十二、ESMFILEFORMAT
URLの特定が完了したら、
Node.jsは次のことを判定します:
这个文件应该按什么格式加载?
.mjs
module
.cjs
commonjs
.json
json
.wasm
wasm
.node
addon
ネイティブ拡張モジュールです。
十三、typeフィールドの役割
対象は次の拡張子を持つ
.js
ファイルです。
Node.jsは最も近いpackage.jsonを探します。
次の設定なら:
このコードは
次のように解釈されます:
ESM
次の設定なら:
このコードは
次のように解釈されます:
CommonJS
したがって、
.mjs
は常にESMです。
.cjs
は常にCommonJSです。
.js
の場合はtypeフィールドに依存します。
十四、ESMがディレクトリのimportをサポートしない理由
CommonJS:
Node.jsは次の候補を試します:
foo.js
foo.json
foo.node
foo/index.js
foo/index.json
ESM:
候補を推測しません。
次の対象が
foo
ディレクトリなら、
直接エラーになります:
Unsupported Directory Import
正しい書き方は次のとおりです:
または:
十五、LOOKUPPACKAGESCOPE
Node.jsは、最も近いpackage.jsonをどのように見つけるのでしょうか。
アルゴリズムは次のとおりです:
当前目录
↓
父目录
↓
继续向上
↓
直到根目录
たとえば:
/app/src/pages/home/index.js
探索先はこの順序です:
/app/src/pages/home/package.json
/app/src/pages/package.json
/app/src/package.json
/app/package.json
最初に見つかったpackage.jsonを採用します。
十六、ESM resolverのカスタマイズ
Node.jsがデフォルトで解決するのは次のようなimportです:
次のようなimportをサポートしたい場合は:
どうすればよいでしょうか。
実現方法は次のとおりです:
次のように実行します:
node --loader ./loader.mjs app.js
実行後、
が自動的に次のパスへマッピングされます:
src/utils.js
十七、完全な解決フロー
Node.jsのESM解決は、次のようにまとめられます:
まとめ
Node.jsのESMで中心になるのは、「ファイルを探すこと」ではありません。
そうではなく、次の順序で解決します:
specifier
↓
确定 URL
↓
确定模块格式
↓
加载执行
exports、imports、type、条件付きexports、loaderの各仕組みも、本質的にはこの目標を中心に設計されています。
ESMの解決モデルはCommonJSより厳格で静的で、ブラウザの挙動にも近しています。また、Vite、Webpack、Rspack、Rollup、Turbopackといった現代のツールチェーンによる解析や最適化にも適しています。