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:

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

Link the local Vite package to the demo project:

cd vite-demo
yarn link vite

Source Code Analysis

Server Creation

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

The CLI loads createServer from src/node/server/index.ts, which builds a Connect middleware stack and starts an HTTP server.

Dependency Pre‑bundling

// 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)
}

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

// 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]
  })
}

The built‑in vite:dep-scan plugin parses HTML, extracts import statements, and records dependencies such as vue.

Transform Middleware

// 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)
}

The middleware calls transformRequest, which reads the file, runs all Vite plugins (including import‑analysis), and returns transformed code.

Import‑analysis Plugin

// 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 }
    }
  }
}

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

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()

Dependency Pre‑bundling Function

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))
}

HTML Middleware

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()
}

Transform Middleware (JS)

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()
}

Import Analysis Helper

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()
}

Transform Middleware (Vue SFC)

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()
}

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Viteesbuilddependency 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

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.