Inside Vite’s Dev Server: How the CLI Boots Up and What Happens Next
This article walks through Vite's CLI startup process, explains how the bin script links to the vite executable, details the devServer's five core modules and fifteen middleware components, and shows the createServer flow that powers Vite's fast, hot‑reloading development experience.
Liang Xiaoying, a front‑end engineer at WeDoctor, shares a deep dive into Vite version 2.2.3.
Analysis version: 2.2.3 – let’s explore the Vite server together.
1. What does the initial CLI start service do?
In
package.jsonthe
binfield points to an executable:
<code>"bin": {
"vite": "bin/vite.js"
}
</code>When the Vite package with a
binfield is installed, the executable is linked into
./node_modules/.bin, and npm creates a symlink from
/usr/local/bin/viteto
vite.js, allowing you to run
vitedirectly from the command line.
Locally you can also run scripts via npm, e.g.
node node_modules/.bin/vite.
What does
vite.jsdo?
The real entry point is
cli.ts, which configures the CLI commands:
<code>import { cac } from 'cac' // CLI helper library
const cli = cac('vite')
cli
.option('-c, --config <file>', `[string] use specified config file`)
.option('-r, --root <path>', `[string] use specified root directory`)
.option('--base <path>', `[string] public base path (default: /)`)
.option('-l, --logLevel <level>', `[string] silent | error | warn | all`)
.option('--clearScreen', `[boolean] allow/disable clear screen when logging`)
.option('-d, --debug [feat]', `[string | boolean] show debug logs`)
.option('-f, --filter <filter>', `[string] filter debug logs`)
// dev command (the focus of this article)
.command('[root]')
.alias('serve')
.option('--host [host]', `[string] specify hostname`)
.option('--port <port>', `[number] specify port`)
.option('--https', `[boolean] use TLS + HTTP/2`)
.option('--open [path]', `[boolean | string] open browser on startup`)
.option('--cors', `[boolean] enable CORS`)
.option('--strictPort', `[boolean] exit if specified port is already in use`)
.option('-m, --mode <mode>', `[string] set env mode`)
.option('--force', `[boolean] force the optimizer to ignore the cache and re‑bundle`)
.action(async (root: string, options) => {
const { createServer } = await import('./server')
const server = await createServer({
root,
base: options.base,
mode: options.mode,
configFile: options.config,
logLevel: options.logLevel,
clearScreen: options.clearScreen,
server: cleanOptions(options) as ServerOptions
})
await server.listen()
})
// build command
.command('build [root]')
// preview command
.command('preview [root]')
// optimize command
.command('optimize [root]')
</code>In short, when you run
npm run dev, the CLI executes
node_modules/vite/dist/node/cli.js, calls
createServerwith the resolved
vite.config.js(or CLI‑provided config), and creates a
viteDevServerinstance.
2. Composition of devServer
The dev server consists of 5 main modules and 15 middleware components:
Debug tip: before digging into the source, start a debug session with
yarn linkor
node --inspect‑brkto step through the server logic.
yarn link local code (see https://cn.vitejs.dev/guide/#command-line-interface)
node --inspect‑brk ./node_modules/.bin/vite --debug lxyDebug
<code>"inspect": "node --inspect-brk ./node_modules/.bin/vite --debug lxyDebug"
</code>Open
chrome://inspectfor debugging (see https://www.ruanyifeng.com/blog/2018/03/node-debugger.html)
3. Five major modules
Below is a brief overview of the five core modules; deeper details will be covered in future articles.
1. WebSocketServer
Uses the ws package to create a WebSocket server via
new WebSocket.Server(), which sends messages and listens for connections. It powers HMR (Hot Module Replacement) communication.
2. watcher – FSWatcher
Vite employs
chokidarto watch file system events. It listens for
add,
unlink, and
changeto update the
moduleGraphand trigger hot updates.
3. ModuleGraph
Tracks import relationships, mapping URLs to files and HMR status. Think of it as a repository that can add, delete, or query modules based on dependency graphs.
4. pluginContainer
Built on Rollup’s plugin container, it provides several hooks:
pluginContainer.watchChange: notifies plugins when a watched file changes.
pluginContainer.resolveId: resolves ES6 import statements to module IDs.
pluginContainer.load: runs each Rollup plugin’s
loadmethod to produce AST data.
pluginContainer.transform: runs each plugin’s
transformto convert source code (e.g., Vue files to JavaScript).
These hooks turn user code into Vite‑compatible code for downstream modules.
5. httpServer
Creates a native Node http / https / http2 server. When HTTPS is enabled, it uses the selfsigned package to generate a self‑signed X509 certificate for secure transport.
4. Fifteen middleware components
Each middleware performs a specific transformation or handling step. Below are the most important ones.
1. timeMiddleware
When --debug is set, this middleware logs the total startup time.
<code>if (process.env.DEBUG) {
middlewares.use(timeMiddleware(root))
}
</code> <code>const logTime = createDebugger('vite:time')
export function timeMiddleware(root) {
return (req, res, next) => {
const start = Date.now()
const end = res.end
res.end = (...args) => {
logTime(`${timeFrom(start)} ${prettifyUrl(req.url, root)}`)
return end.call(res, ...args)
}
next()
}
}
</code>2. corsMiddleware
Handles CORS based on the cors option in vite.config.js using the cors package.
<code>import corsMiddleware from 'cors'
const { cors } = serverConfig
if (cors !== false) {
middlewares.use(corsMiddleware(typeof cors === 'boolean' ? {} : cors))
}
</code>3. proxyMiddleware
Implements proxying via the http-proxy package, respecting vite.config.js proxy settings.
<code>export function proxyMiddleware(httpServer, config) {
const options = config.server.proxy!
const proxy = httpProxy.createProxyServer(opts)
// ...handle upgrade for websockets and http requests
return (req, res, next) => {
// match context, apply bypass, rewrite, then proxy.web(...)
}
}
</code>4. baseMiddleware
Normalizes request URLs based on the configured base path.
<code>export function baseMiddleware({ config }) {
const base = config.base
return (req, res, next) => {
const url = req.url!
const parsed = parseUrl(url)
const path = parsed.pathname || '/'
if (path.startsWith(base)) {
req.url = url.replace(base, '/')
} else if (path === '/' || path === '/index.html') {
res.writeHead(302, { Location: base })
res.end()
return
} else if (req.headers.accept?.includes('text/html')) {
res.statusCode = 404
res.end()
return
}
next()
}
}
</code>5. launchEditorMiddleware
Opens a file in the editor at a specific line via the launch-editor-middleware package.
<code>import launchEditorMiddleware from 'launch-editor-middleware'
middlewares.use('/__open-in-editor', launchEditorMiddleware())
</code>6. pingPongMiddleware
Provides a simple heartbeat endpoint for HMR reconnection.
<code>middlewares.use('/__vite_ping', (_, res) => res.end('pong'))
</code>7. decodeURIMiddleware
Decodes URL‑encoded request paths before the static file middleware runs.
<code>middlewares.use(decodeURIMiddleware())
</code>8. servePublicMiddleware
Serves static assets from the public directory before other transforms.
<code>middlewares.use(servePublicMiddleware(config.publicDir))
</code>9. transformMiddleware
Core transformer that adds the request URL to the moduleGraph , performs caching, loads, and transforms code via plugins. It returns an object containing code , map , and etag .
<code>mod.transformResult = {
code, // transformed code from plugins
map, // source map
etag: getEtag(code, { weak: true })
}
</code>Key steps: Resolve ID via pluginContainer.resolveId(url)?.id Load source with pluginContainer.load(id) Update moduleGraph and watch the file Inject source content when needed Return the assembled result Examples of transformed outputs are shown with screenshots for JS, import‑based HMR updates, and CSS handling. 10. serveRawFsMiddleware Handles URLs prefixed with /@fs/ by stripping the prefix and serving the original file from the filesystem. <code>export function serveRawFsMiddleware() { const isWin = os.platform() === 'win32' const serveFromRoot = sirv('/', sirvOptions) return (req, res, next) => { let url = req.url! if (url.startsWith(FS_PREFIX)) { url = url.slice(FS_PREFIX.length) if (isWin) url = url.replace(/^[A-Z]:/i, '') req.url = url serveFromRoot(req, res, next) } else { next() } } } </code> 11. serveStaticMiddleware Serves static files from a given directory, applying Vite’s alias resolution before serving. <code>export function serveStaticMiddleware(dir, config) { const serve = sirv(dir, sirvOptions) return (req, res, next) => { const url = req.url! if (path.extname(cleanUrl(url)) === '.html') return next() // apply alias redirects let redirected for (const { find, replacement } of config.resolve.alias) { const matches = typeof find === 'string' ? url.startsWith(find) : find.test(url) if (matches) { redirected = url.replace(find, replacement); break } } if (redirected) { if (redirected.startsWith(dir)) redirected = redirected.slice(dir.length) req.url = redirected } serve(req, res, next) } } </code> 12. spaMiddleware Provides HTML5 history‑API fallback for single‑page applications, serving index.html for unknown routes. <code>import history from 'connect-history-api-fallback' if (!middlewareMode) { middlewares.use( history({ logger: createDebugger('vite:spa-fallback'), rewrites: [{ from: /\/\/$/, to({ parsedUrl }) { const rewritten = parsedUrl.pathname + 'index.html' return fs.existsSync(path.join(root, rewritten)) ? rewritten : '/index.html' } }] }) ) } </code> 13. indexHtmlMiddleware Transforms the entry index.html file before it is served. <code>if (!middlewareMode) { middlewares.use(indexHtmlMiddleware(server)) } </code> 14. 404Middleware Handles unmatched requests with a 404 response. <code>if (!middlewareMode) { middlewares.use((_, res) => { res.statusCode = 404 res.end() }) } </code> 15. errorMiddleware Logs internal server errors, sends them over the WebSocket, and returns a 500 response unless allowNext is true. <code>export function errorMiddleware(server, allowNext = false) { return (err, _req, res, next) => { const msg = buildErrorMessage(err, [chalk.red(`Internal server error: ${err.message}`)]) server.config.logger.error(msg, { clear: true, timestamp: true }) server.ws.send({ type: 'error', err: prepareError(err) }) if (allowNext) next() else { res.statusCode = 500; res.end() } } } </code> 5. Summary of createServer The article started from the initial CLI command, explained what the Vite executable does, and guided readers to the createServer entry point. It then dissected the five core modules (WebSocketServer, watcher, ModuleGraph, pluginContainer, httpServer) and the fifteen middleware pieces that together form the devServer pipeline. By leveraging native ES modules, Vite achieves fast cold starts and efficient hot‑module replacement, delivering a smooth development experience. Overall, Vite’s architecture shows no bundling step; instead, it relies on on‑the‑fly transformation via Rollup plugins and a well‑orchestrated middleware chain.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.