Frontend Development 19 min read

Building a Minimal Vite Dev Server from Scratch: A Step‑by‑Step Guide

This article walks through the source‑code analysis of Vite, explains why Vite rewrites import paths and creates a .vite folder, and provides a complete, minimal implementation of a Vite‑like development server using esbuild, connect middleware, and Vue SFC compilation.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Building a Minimal Vite Dev Server from Scratch: A Step‑by‑Step Guide

Introduction

The goal is to implement a ultra‑light version of Vite that can start a dev server, pre‑bundle dependencies, and transform

.js

and

.vue

files so they run in the browser. The final simple Vite can be downloaded from the GitHub repository linked in the article.

Problems to Solve

When running a Vite project, developers notice three main phenomena:

The import statement

import { createApp } from 'vue'

is rewritten to

import { createApp } from '/node_modules/.vite/vue.js'

.

A

.vite

folder appears under

node_modules

.

.vue files are served as JavaScript after transformation.

These issues are addressed by analyzing Vite’s source code and reproducing the essential logic without hot‑module replacement.

Preparation

Clone the Vite repository and create a symlink:

<code>git clone [email protected]:vitejs/vite.git
cd vite && yarn
cd packages/vite && yarn build && yarn link
yarn dev</code>

Link the local Vite package to the demo project:

<code>cd vite-demo
yarn link vite</code>

Source Code Analysis

Server Creation

<code>// src/node/cli.ts
cli.command('[root]')
  .alias('serve')
  .action(async () => {
    const { createServer } = await import('./server')
    const server = await createServer({ /* ... */ })
    await server.listen()
  })</code>

The CLI loads

createServer

from

src/node/server/index.ts

, which builds a Connect middleware stack and starts an HTTP server.

Dependency Pre‑bundling

<code>// src/node/optimizer/index.ts
if (config.cacheDir) {
  server._isRunningOptimizer = true
  try {
    server._optimizeDepsMetadata = await optimizeDeps(config)
  } finally {
    server._isRunningOptimizer = false
  }
  server._registerMissingImport = createMissingImporterRegisterFn(server)
}</code>

The

optimizeDeps

function uses

esbuild

to scan imports, bundle them, and write the results to

node_modules/.vite

. This explains the extra

.vite

folder.

Import Analysis

<code>// src/node/optimizer/scan.ts
import { Loader, Plugin, build, transform } from 'esbuild'
export async function scanImports() {
  const entry = await globEntries('**/*.html', config)
  const plugin = esbuildScanPlugin()
  await build({
    write: false,
    entryPoints: [entry],
    bundle: true,
    format: 'esm',
    plugins: [plugin]
  })
}</code>

The built‑in

vite:dep-scan

plugin parses HTML, extracts

import

statements, and records dependencies such as

vue

.

Transform Middleware

<code>// src/node/server/middlewares/transform.ts
if (isJSRequest(url)) {
  const result = await transformRequest(url)
  return send(req, res, result.code, type, result.etag,
    isDep ? 'max-age=31536000,immutable' : 'no-cache',
    result.map)
}</code>

The middleware calls

transformRequest

, which reads the file, runs all Vite plugins (including

import‑analysis

), and returns transformed code.

Import‑analysis Plugin

<code>// src/node/plugins/importAnalysis.ts
export function importAnalysisPlugin() {
  return {
    name: 'vite:import-analysis',
    async transform(source, importer, ssr) {
      const specifiers = parseImports(source)
      for (const { n, s, e } of specifiers) {
        const resolved = await this.resolve(n)
        const replacePath = resolved.id
        source = source.slice(0, s) + replacePath + source.slice(e)
      }
      return { code: source }
    }
  }
}</code>

This plugin uses

es‑module‑lexer

to locate import specifiers, resolves them to the pre‑bundled files, and rewrites the import paths.

Implementation

Server Skeleton

<code>const http = require('http')
const connect = require('connect')
const middlewares = connect()
async function createServer() {
  await optimizeDeps()
  http.createServer(middlewares).listen(3000, () => {
    console.log('simple-vite-dev-server start at localhost:3000!')
  })
}
middlewares.use(indexHtmlMiddleware)
middlewares.use(transformMiddleware)
createServer()</code>

Dependency Pre‑bundling Function

<code>const fs = require('fs')
const path = require('path')
const esbuild = require('esbuild')
const cacheDir = path.join(__dirname, '../node_modules/.vite')
async function optimizeDeps() {
  if (fs.existsSync(cacheDir)) return false
  fs.mkdirSync(cacheDir, { recursive: true })
  const deps = Object.keys(require('../package.json').dependencies)
  const result = await esbuild.build({
    entryPoints: deps,
    bundle: true,
    format: 'esm',
    logLevel: 'error',
    splitting: true,
    sourcemap: true,
    outdir: cacheDir,
    treeShaking: 'ignore-annotations',
    metafile: true,
    define: { 'process.env.NODE_ENV': '"development"' }
  })
  const outputs = Object.keys(result.metafile.outputs)
  const data = {}
  deps.forEach(dep => {
    data[dep] = '/' + outputs.find(o => o.endsWith(`${dep}.js`))
  })
  const dataPath = path.join(cacheDir, '_metadata.json')
  fs.writeFileSync(dataPath, JSON.stringify(data, null, 2))
}
</code>

HTML Middleware

<code>const indexHtmlMiddleware = (req, res, next) => {
  if (req.url === '/') {
    const htmlPath = path.join(__dirname, '../index.html')
    const html = fs.readFileSync(htmlPath, 'utf-8')
    res.setHeader('Content-Type', 'text/html')
    res.statusCode = 200
    return res.end(html)
  }
  next()
}
</code>

Transform Middleware (JS)

<code>const transformMiddleware = async (req, res, next) => {
  if (req.url.endsWith('.js') || req.url.endsWith('.map')) {
    const jsPath = path.join(__dirname, '../', req.url)
    const code = fs.readFileSync(jsPath, 'utf-8')
    res.setHeader('Content-Type', 'application/javascript')
    res.statusCode = 200
    const transformed = req.url.endsWith('.map') ? code : await importAnalysis(code)
    return res.end(transformed)
  }
  next()
}
</code>

Import Analysis Helper

<code>const { init, parse } = require('es-module-lexer')
const MagicString = require('magic-string')
const cacheDir = path.join(__dirname, '../node_modules/.vite')
async function importAnalysis(code) {
  await init
  const [imports] = parse(code)
  if (!imports || !imports.length) return code
  const meta = require(path.join(cacheDir, '_metadata.json'))
  let magic = new MagicString(code)
  imports.forEach(({ n, s, e }) => {
    const replace = meta[n] || n
    magic = magic.overwrite(s, e, replace)
  })
  return magic.toString()
}
</code>

Transform Middleware (Vue SFC)

<code>const compileSFC = require('@vue/compiler-sfc')
const compileDom = require('@vue/compiler-dom')
const transformMiddleware = async (req, res, next) => {
  if (req.url.includes('.vue')) {
    const vuePath = path.join(__dirname, '../', req.url.split('?')[0])
    const content = fs.readFileSync(vuePath, 'utf-8')
    const { descriptor } = compileSFC.parse(content)
    const script = descriptor.script.content.replace('export default ', 'const __script = ')
    const tpl = compileDom.compile(descriptor.template.content, { mode: 'module' }).code
    const tplReplaced = tpl.replace('export function render(_ctx, _cache)', '__script.render=(_ctx,_cache)=>')
    const finalCode = `
${await importAnalysis(script)}
${tplReplaced}
export default __script;
`
    res.setHeader('Content-Type', 'application/javascript')
    res.statusCode = 200
    return res.end(await importAnalysis(finalCode))
  }
  next()
}
</code>

Conclusion

The minimal Vite implementation demonstrates two core ideas: using

esbuild

for fast dependency pre‑bundling to convert CommonJS/AMD modules into browser‑compatible ES modules, and a

transformMiddleware

that rewrites import paths and compiles Vue single‑file components into executable JavaScript.

frontend developmentVueViteDev Serveresbuilddependency pre-bundling
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.