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​
- Speed: Native ESM, esbuild for pre-bundling
- HMR: Instant hot module replacement
- TypeScript: Built-in TypeScript support
- React: Official React plugin
- WASM: Native WASM support
- Code Splitting: Automatic chunk optimization
- 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​
| Metric | Target | Vite 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/