Skip to main content

ADR-012: Use Vite for Build Tool

Date: 2025-10-06 Status: Accepted Deciders: Development Team Tags: build, tooling, performance

Context​

The IDE requires a modern build tool with:

  • Fast development server (HMR)
  • TypeScript support
  • React support
  • WASM integration
  • Code splitting
  • Production optimization
  • Modern browser targets

Decision​

We will use Vite 5.0 as the build tool and development server.

Rationale​

Why Vite​

  1. Speed: Native ESM, esbuild for pre-bundling
  2. HMR: Instant hot module replacement
  3. TypeScript: Built-in TypeScript support
  4. React: Official React plugin
  5. WASM: Native WASM support
  6. Code Splitting: Automatic chunk optimization
  7. Modern: ES2020+ output, top-level await

Performance Benefits​

  • Dev Server Start: < 1s (vs 10-30s for Webpack)
  • HMR Update: < 50ms (vs 1-5s for Webpack)
  • Build Time: 5-10x faster than Webpack
  • Bundle Size: Optimized chunks, tree-shaking

Alternatives Considered​

Webpack 5​

  • Pros: Mature, extensive ecosystem, widely used
  • Cons: Slow dev server, complex config, slower HMR
  • Rejected: Vite significantly faster for development

Parcel​

  • Pros: Zero config, fast, good caching
  • Cons: Less control, smaller ecosystem
  • Rejected: Less mature plugin ecosystem than Vite

esbuild​

  • Pros: Extremely fast, Go-based
  • Cons: Limited features, no dev server, immature
  • Rejected: Vite uses esbuild under the hood

Rollup​

  • Pros: Great for libraries, tree-shaking
  • Cons: Slower dev server, more config needed
  • Rejected: Vite uses Rollup for production builds

Turbopack (Next.js)​

  • Pros: Very fast, Rust-based
  • Cons: Tied to Next.js, not standalone
  • Rejected: Vite more flexible for custom IDE

Consequences​

Positive​

  • Lightning-fast dev server and HMR
  • Simple configuration
  • Modern JavaScript features (top-level await, import.meta)
  • Excellent TypeScript support
  • First-class WASM support
  • Good plugin ecosystem

Negative​

  • Newer than Webpack (less mature)
  • Some plugins may not exist yet
  • Production build uses different bundler (Rollup)
  • Dev and prod can have slight differences

Neutral​

  • Need to learn Vite-specific patterns
  • Different from traditional bundler config
  • ES modules only (no CJS in browser)

Implementation​

Vite Configuration​

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
import monacoeditorPlugin from 'vite-plugin-monaco-editor';

export default defineConfig({
plugins: [
react({
// Fast Refresh
fastRefresh: true,
// Babel plugins
babel: {
plugins: ['babel-plugin-macros']
}
}),
wasm(),
topLevelAwait(),
monacoeditorPlugin({
languageWorkers: ['typescript', 'json', 'css', 'html']
})
],

// Development server
server: {
port: 5173,
host: true,
fs: {
allow: ['..']
},
proxy: {
'/api': 'http://localhost:3001'
}
},

// Build configuration
build: {
target: 'esnext',
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
// Monaco editor in separate chunk
monaco: ['monaco-editor', '@monaco-editor/react'],
// terminal in separate chunk
terminal: ['xterm', 'xterm-addon-fit'],
// Vendor chunk
vendor: ['react', 'react-dom', '@chakra-ui/react'],
}
}
},
// Minification
minify: 'esbuild',
chunkSizeWarningLimit: 1000
},

// Optimize dependencies
optimizeDeps: {
include: ['react', 'react-dom', '@chakra-ui/react'],
exclude: ['@xterm/xterm', 'monaco-editor'],
esbuildOptions: {
target: 'esnext',
supported: {
'top-level-await': true
}
}
},

// Environment variables
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
__APP_version__: JSON.stringify(process.env.npm_package_version)
},

// Path resolution
resolve: {
alias: {
'@': '/src',
'@components': '/src/components',
'@services': '/src/services',
'@store': '/src/store',
'@hooks': '/src/hooks',
'@utils': '/src/utils',
'@types': '/src/types'
}
}
});

Package.json Scripts​

{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"analyze": "vite-bundle-visualizer"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8",
"vite-plugin-wasm": "^3.3.0",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-bundle-visualizer": "^1.0.0"
}
}

TypeScript Configuration​

{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,

/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@services/*": ["./src/services/*"],
"@store/*": ["./src/store/*"],
"@hooks/*": ["./src/hooks/*"],
"@utils/*": ["./src/utils/*"],
"@types/*": ["./src/types/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

Environment Variables​

// src/vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_LM_STUDIO_API: string;
readonly VITE_APP_NAME: string;
readonly VITE_ENABLE_TERMINAL: string;
readonly VITE_DEFAULT_THEME: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}

Code Splitting​

// Lazy load components
import { lazy, Suspense } from 'react';

const Monaco = lazy(() => import('@monaco-editor/react'));
const Xterminal = lazy(() => import('./components/terminal/Xterminal'));
const Settings = lazy(() => import('./components/Settings/Settings'));

// Usage
<Suspense fallback={<Spinner />}>
<Monaco {...props} />
</Suspense>

WASM Integration​

// Import WASM modules
import init, { process_data } from './wasm/rust_module.wasm?init';

// Initialize WASM
await init();

// Use WASM functions
const result = process_data(inputData);

Asset Handling​

// Import assets
import logoUrl from './assets/logo.png';
import styleUrl from './assets/styles.css?url';
import svgContent from './assets/icon.svg?raw';

// Use in components
<img src={logoUrl} alt="Logo" />

Hot Module Replacement​

// Accept HMR updates
if (import.meta.hot) {
import.meta.hot.accept('./store/editorStore', (newModule) => {
// Handle store updates without full reload
console.log('editor store updated');
});

import.meta.hot.dispose(() => {
// Cleanup before module reload
cleanup();
});
}

Build Optimization​

Production Build​

# Build for production
npm run build

# Output:
# dist/
# ├── assets/
# │ ├── index-[hash].js # Main bundle
# │ ├── monaco-[hash].js # Monaco chunk
# │ ├── terminal-[hash].js # terminal chunk
# │ ├── vendor-[hash].js # Vendor chunk
# │ └── [other chunks]
# ├── index.html
# └── assets.json

Bundle Analysis​

# Analyze bundle size
npm run analyze

# Opens visualization showing:
# - Chunk sizes
# - Module dependencies
# - Import paths

Performance Targets​

MetricTargetVite Capability
Dev server start< 1s~500ms
HMR update< 100ms~20-50ms
Production build< 30s~15-20s
Initial bundle< 500KB~300KB (gzipped)
Chunk load< 200ms~100ms

Docker Integration​

# Dockerfile
FROM node:20-slim

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Copy source
COPY . .

# Expose Vite dev server port
EXPOSE 5173

# Run dev server
CMD ["npm", "run", "dev", "--", "--host"]

Continuous Integration​

# .github/workflows/build.yml
name: Build

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist/

References​