What Is a Resolver?
For:
import Button from "@/components/Button";
At this point, the JavaScript engine sees only this:
specifier = "@/components/Button"
The problem we need to solve is mapping the specifier to the real module:
specifier
↓
真实模块
This process is called resolution.
The entry algorithm in the Node.js ESM specification can be summarized as:
ESM_RESOLVE(specifier, parentURL)
Its job is to complete this mapping.
Which Specifiers Does Node Support by Default?
Node's built-in resolver supports only four kinds of specifier:
import "./foo.js"
import "../foo.js"
import "/app/foo.js"
Path specifiers.
Package specifiers.
Package Imports.
URL specifiers.
Apart from these:
Node cannot resolve it and instead follows the error path:
Module Not Found
Why Aliases Work
Suppose you have:
Webpack configuration:
Before performing the actual resolution, Webpack first runs the specifier through the alias resolver:
@/components/Button
It then proceeds with normal module resolution.
So an alias isn't a new type of module. It simply rewrites the old specifier as a new one:
旧 specifier
↓
新 specifier
Vite Alias
Vite:
The resolution flow is exactly the same as Webpack's.
For:
Vite first performs:
@ -> /project/src
yielding:
/project/src/utils/request
It then continues with the remaining resolution steps.
Therefore:
Webpack Alias
Vite Alias
Rspack Alias
Webpack Alias, Vite Alias, and Rspack Alias are all fundamentally the same thing.
The only difference is the resolver implementation.
Why TypeScript Paths Often Fail
Configuration:
Many people find that VSCode and TypeScript work, but Node fails at runtime:
VSCode 正常
TypeScript 正常
Node 运行失败
The reason is simple:
paths
It isn't a runtime resolver.
TypeScript only uses paths during type checking, IDE navigation, and compilation:
类型检查
IDE 跳转
编译阶段
Node's runtime never reads:
So this configuration:
cannot make Node recognize:
You must additionally configure one of the following:
Webpack Alias
Vite Alias
Rspack Alias
Node Loader
How a Node Loader Implements an Alias
Node provides:
This lets you take over the resolution process.
For example:
At this point, the import:
is transformed into:
file:///project/src/utils.js
and then enters the normal loading process.
Browser Import Map
Browsers don't have:
node_modules
So this import won't work on its own:
An import map defines these mappings:
When the browser encounters:
it first maps:
react
↓
/vendor/react.js
and then continues loading.
The Essence of a Resolver
Look at:
Webpack Alias
Vite Alias
Rspack Alias
TS Paths
Node Loader
Import Map
and you'll see that they all do the same thing:
specifier
↓
mapping
↓
new specifier
↓
real URL
The only difference is where the mapping rules are stored:
映射规则存放的位置不同
The Starting Point of the Module Graph
For:
The compiler first resolves:
specifier
↓
resolver
↓
real module
yielding:
App.tsx
Then it continues recursively:
App.tsx
↓
Button.tsx
↓
request.ts
↓
...
Ultimately, it builds:
Module Graph
Tree shaking, code splitting, HMR, and chunk splitting all build on top of this graph.
Therefore, the resolver is the true entry point to the entire modern frontend toolchain.