Why Vite, Webpack, and Rspack Can Implement @ Aliases
What is a Resolver?
For:
import Button from "@/components/Button";
The JavaScript engine actually only sees this:
specifier = "@/components/Button"
The problem that needs to be solved next is:
specifier
↓
real module
This process is called Resolution.
The entry algorithm in Node.js ESM specification:
ESM_RESOLVE(specifier, parentURL)
Essentially completes this mapping process.
What Specifiers Does Node Support by Default?
Node's native Resolver only supports four types:
import "./foo.js"
import "../foo.js"
import "/app/foo.js"
Path modules.
import react from "react"
Package modules.
import db from "#db"
Package Imports.
import x from "file:///app/a.js"
URL modules.
Beyond this:
import x from "@/utils"
Node cannot resolve this.
Directly enters:
Module Not Found
The exception path.
Why Alias Works
Assuming we have:
import Button from "@/components/Button"
Webpack configuration:
resolve: {
alias: {
"@": "/project/src"
}
}
Before Webpack performs the actual resolution:
@/components/Button
It first goes through the Alias Resolver:
/project/src/components/Button
Then proceeds to normal module resolution.
Therefore, Alias is not a new module type.
Instead, it is a transformation:
old specifier
↓
new specifier
Vite Alias
Vite:
resolve: {
alias: {
"@": path.resolve(__dirname, "src")
}
}
The execution flow is exactly the same as Webpack.
For:
import x from "@/utils/request"
Vite first executes:
@ -> /project/src
To get:
/project/src/utils/request
Then continues with subsequent resolution.
Therefore:
Webpack Alias
Vite Alias
Rspack Alias
Are essentially the same thing.
The difference is only the Resolver implementation.
Why TypeScript Paths Often Fail
Configuration:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
Many people find:
VSCode works
TypeScript works
Node execution fails
The reason is:
paths
Is not a runtime Resolver.
TypeScript only uses paths during:
type checking
IDE navigation
compilation phase
Node at runtime doesn't read:
tsconfig.json
Therefore:
{
"paths": {}
}
Cannot make Node recognize:
import "@/utils"
Must additionally configure one of:
Webpack Alias
Vite Alias
Rspack Alias
Node Loader
How Node Loader Implements Alias
Node provides:
export async function resolve(
specifier,
context,
nextResolve
)
Allows intercepting the resolution process.
For example:
export async function resolve(
specifier,
context,
nextResolve
) {
if (specifier.startsWith("@/")) {
return {
url: pathToFileURL(
path.resolve(
process.cwd(),
"src",
specifier.slice(2)
)
).href,
shortCircuit: true
}
}
return nextResolve(specifier, context)
}
At this point:
import x from "@/utils"
Will become:
file:///project/src/utils.js
Then enters the normal loading flow.
Browser Import Map
Browsers don't have:
node_modules
Therefore:
import React from "react"
Won't work.
The purpose of Import Map:
<script type="importmap">
{
"imports": {
"react": "/vendor/react.js"
}
}
</script>
When executing:
import React from "react"
The browser first maps:
react
↓
/vendor/react.js
Then continues loading.
The Essence of Resolver
Observing:
Webpack Alias
Vite Alias
Rspack Alias
TS Paths
Node Loader
Import Map
You'll find they all do the same thing:
specifier
↓
mapping
↓
new specifier
↓
real URL
The difference is only:
Where the mapping rules are stored
The Starting Point of Module Graph
For:
import App from "./App"
The compiler first executes:
specifier
↓
resolver
↓
real module
To get:
App.tsx
Then continues recursively:
App.tsx
↓
Button.tsx
↓
request.ts
↓
...
Finally builds:
Module Graph
Tree Shaking, Code Splitting, HMR, and Chunk splitting all build on top of this graph.
Therefore, Resolver is the true entry point of the entire modern frontend toolchain.