The Node.js ESM resolution algorithm has just two responsibilities:
- Resolve the specifier in an
importto a final URL - Determine the module format for that URL
For example:
Node.js ultimately needs to resolve these to:
react -> file:///project/node_modules/react/index.js
./utils.js -> file:///project/src/utils.js
#config -> file:///project/src/config/index.js
It must also determine the module format:
module
commonjs
json
wasm
These are among the possible module formats.
At its core, the entire ESM specification describes:
specifier
↓
resolved URL
↓
module format
↓
load
↓
execute
1. The ESM_RESOLVE Process
The entry-point algorithm for Node.js ESM is:
ESM_RESOLVE(specifier, parentURL)
It can be simplified to:
The algorithm's central goal is:
specifier
↓
唯一 URL
rather than repeatedly guessing, as CommonJS does.
2. Specifier Categories
Node.js divides specifiers into four categories.
1. URL Specifier
For example:
Or:
If the specifier is already a valid URL:
new URL(specifier)
The operation succeeds.
Then it is returned directly.
2. Relative Specifier
For example:
Node.js resolves the specifier based on the location of the current module.
Suppose:
file:///app/src/main.js
Executing:
produces:
file:///app/src/utils.js
Under the hood, this is:
3. Package Imports
For example:
Or:
A specifier that starts with:
#
enters:
PACKAGE_IMPORTS_RESOLVE()
and causes Node.js to read the current package:
For example:
ultimately resolves to:
./src/db/index.js
4. Bare Specifier
For example:
If it is none of the following:
URL
路径
#import
it is a bare package specifier.
Resolution enters:
PACKAGE_RESOLVE()
and starts searching node_modules.
3. PACKAGE_RESOLVE
This is the core logic Node.js uses to locate an npm package.
For example:
Given the current file:
/app/src/pages/home/index.js
Node.js searches upward through these directories:
/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
until it finds the package.
Equivalent to:
while(currentDirectory){
查找 node_modules/packageName
找不到继续向上
}
4. Reading package.json
After the package is found:
node_modules/react/package.json
Node.js begins reading its configuration.
The precedence is:
exports
↓
main
↓
直接路径
5. The exports Mechanism
Modern Node.js package resolution relies almost entirely on exports.
For example:
This allows:
which corresponds to:
.
and resolves to:
./dist/index.js
This also allows:
which corresponds to:
./jsx-runtime
and resolves to:
./dist/jsx-runtime.js
However:
If exports does not contain:
Node.js reports the following error:
Package Path Not Exported
6. What exports Really Means
exports can be thought of as:
包对外暴露的公开 API
For example:
This allows:
but blocks:
even if the file physically exists.
7. Conditional Exports
The exports value need not be a string.
It can be an object.
For example:
Node.js selects a target based on the active conditions.
For ESM:
it matches:
and resolves to:
./dist/index.mjs
For CommonJS:
it matches:
and resolves to:
./dist/index.cjs
8. PACKAGESELFRESOLVE
Suppose:
The current code is inside:
my-lib
Then:
Node.js does not search an external node_modules directory.
Instead, it detects that:
当前包名就是 my-lib
and directly uses the exports in the current package.json.
This is:
PACKAGE_SELF_RESOLVE
9. The imports Mechanism
imports and exports are very similar.
The difference is:
exports 给别人用
imports 给自己用
For example:
Then:
actually resolves to:
./src/db/index.js
Another example:
resolves to:
./src/utils/string.js
10. Pattern Matching
Both imports and exports support wildcards.
For example:
When you run:
the wildcard:
*
matches:
user
The final target is:
./src/features/user.js
11. PACKAGETARGETRESOLVE
This is where exports/imports mappings are actually applied.
Four target types are supported.
String
The target is resolved directly.
Object
Node.js selects the target according to the conditions.
Array
If the first target fails, Node.js tries the next one.
null
This explicitly prevents the subpath from being exported.
12. ESMFILEFORMAT
Once the URL has been resolved,
Node.js must determine:
这个文件应该按什么格式加载?
.mjs
module
.cjs
commonjs
.json
json
.wasm
wasm
.node
addon
A native extension module.
13. The Role of the type Field
For a:
.js
file.
Node.js looks for the nearest package.json.
For example:
Then:
is interpreted as:
ESM
If:
then:
is interpreted as:
CommonJS
Therefore:
.mjs
is always ESM.
.cjs
is always CommonJS.
.js
depends on the type field.
14. Why ESM Does Not Support Directory Imports
CommonJS:
Node.js tries:
foo.js
foo.json
foo.node
foo/index.js
foo/index.json
ESM:
does not guess.
If:
foo
is a directory.
It reports the following error directly:
Unsupported Directory Import
The correct form is:
or:
15. LOOKUPPACKAGESCOPE
How does Node.js find the nearest package.json?
The algorithm is:
当前目录
↓
父目录
↓
继续向上
↓
直到根目录
For example, given:
/app/src/pages/home/index.js
it checks:
/app/src/pages/home/package.json
/app/src/pages/package.json
/app/src/package.json
/app/package.json
and stops at the first match.
16. Custom ESM Resolver
By default, Node.js resolves:
If you want to support:
what should you do?
The answer is:
Loader Hook
For example:
Run:
node --loader ./loader.mjs app.js
Then:
is automatically mapped to:
src/utils.js
17. The Complete Resolution Chain
Node.js ESM resolution can be summarized as:
Summary
The core idea behind Node.js ESM resolution is not “finding a file.”
Instead:
specifier
↓
确定 URL
↓
确定模块格式
↓
加载执行
The exports, imports, type, conditional exports, and loader mechanisms all exist to support this goal.
Compared with CommonJS, the ESM resolution model is stricter, more static, and closer to how browsers work, making it better suited to analysis and optimization by modern toolchains (Vite, Webpack, Rspack, Rollup, Turbopack).