Why Vite, Webpack, and Rspack Can Implement the @ Alias
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.
Which 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 that:
import x from "@/utils"
Node cannot resolve this.
It directly goes to:
Module Not Found
The exception path.
Why Does Alias Work
Suppose 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 it proceeds to normal module resolution.
Therefore, Alias is not a new module type.
Instead, it's 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
Getting:
/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 in the Resolver implementation.
Why TypeScript Paths Often Fail
Configuration:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
Many people find:
VSCode works
TypeScript works
Node runtime fails
The reason is:
paths
Is not a runtime Resolver.
TypeScript only uses paths during:
type checking
IDE navigation
compilation phase
Node runtime doesn't read:
tsconfig.json
At all.
Therefore:
{
"paths": {}
}
Cannot make Node recognize:
import "@/utils"
You 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
)
Which allows taking over 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"
Becomes:
file:///project/src/utils.js
Then proceeds to 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
Observe:
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
Getting:
App.tsx
Then continues recursively:
App.tsx
↓
Button.tsx
↓
request.ts
↓
...
Finally building:
Module Graph
Tree Shaking, Code Splitting, HMR, and Chunk Splitting are all built on top of this graph.
Therefore, Resolver is the true entry point of the entire modern frontend toolchain.