
Vite
- 91 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
vite is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- vite
- AI & Agent Building
- AI-coding skill
Vite by the numbers
- 91 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,798 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill viteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 91 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Vite
Overview
Vite is a next-generation frontend build tool that provides instant dev server start via native ES modules and optimized production builds via Rollup. It supports TypeScript, JSX, CSS preprocessors, and static assets out of the box with zero configuration.
When to use: Single-page apps, multi-page apps, library publishing, SSR applications, monorepo packages, any modern frontend project needing fast dev feedback and optimized builds.
When NOT to use: Legacy browsers requiring ES5 output without transpilation, projects locked to Webpack-specific loaders with no Vite equivalents, non-JavaScript build pipelines.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| Config file | defineConfig({}) | Type-safe config with IDE support |
| Conditional config | defineConfig(({ command, mode }) => ({})) | Different config per command/mode |
| Path alias | resolve.alias | Map @/ to src/ |
| Dev proxy | server.proxy | Forward API requests to backend |
| HMR config | server.hmr | WebSocket host/port/protocol |
| HTTPS dev | server.https | Pass TLS cert/key options |
| Build target | build.target | ES module target for output |
| Manual chunks | build.rollupOptions.output.manualChunks | Control code splitting |
| Library mode | build.lib | Publish ES/CJS/UMD packages |
| SSR build | build.ssr + ssr options | Server-side rendering config |
| Env variables | import.meta.env.VITE_* | Client-exposed env vars |
| loadEnv | loadEnv(mode, root, prefix) | Load env vars in config |
| CSS modules | css.modules | Scoped CSS class names |
| Preprocessors | css.preprocessorOptions | Sass/Less/Stylus options |
| PostCSS | css.postcss | Inline or external PostCSS config |
| Static assets | import url from './img.png' | Returns resolved public URL |
| Plugin | { name, transform, load } | Hook-based plugin system |
| Virtual module | resolveId + load hooks | Generate modules at build time |
| Multi-page | build.rollupOptions.input | Multiple HTML entry points |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Exposing secrets via VITE_ prefix | Only prefix client-safe vars with VITE_; use loadEnv in config for server-only secrets |
Using process.env in client code | Use import.meta.env.VITE_* (Vite replaces at build time) |
Modifying rollupOptions.input without resolve() | Always use path.resolve() or import.meta.dirname for absolute paths |
| Not externalizing peer deps in library mode | Add React/Vue to rollupOptions.external to avoid bundling |
| Creating QueryClient-style singletons in SSR | Ensure per-request state in SSR to avoid cross-request leaks |
Inline PostCSS config alongside postcss.config.js | Use one or the other; inline config disables config file search |
Setting base without trailing slash | base should be /path/ with trailing slash or full URL |
Using __dirname in ESM config | Use import.meta.dirname (Node 21+) or fileURLToPath |
Delegation
- Plugin discovery: Use
Exploreagent - Build analysis: Use
Taskagent - Config review: Delegate to
code-revieweragent
If the vitest-testing skill is available, delegate test configuration and Vitest setup to it.If the tailwind skill is available, delegate Tailwind CSS configuration and utility patterns to it.If the react-patterns skill is available, delegate React component patterns and hooks to it.If the typescript-patterns skill is available, delegate TypeScript configuration and type patterns to it.If the pnpm-workspace skill is available, delegate monorepo workspace configuration to it.References
- Configuration fundamentals and defineConfig patterns
- Plugin authoring and popular plugins
- Dev server setup: proxy, HMR, HTTPS, and middleware
- Build optimization: chunking, tree-shaking, and output control
- Library mode for publishing packages
- SSR configuration and Express integration
- Environment variables and .env file handling
- CSS handling: PostCSS, CSS modules, preprocessors, and asset management
Build Optimization
Build Defaults
Vite produces optimized production builds with sensible defaults:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'es2020',
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
minify: 'esbuild',
cssMinify: 'esbuild',
},
});| Option | Default | Purpose |
|---|---|---|
target | 'modules' | Browser target ('es2020', 'esnext', 'chrome100') |
outDir | 'dist' | Output directory |
assetsDir | 'assets' | Nested directory for generated assets |
sourcemap | false | true, 'inline', or 'hidden' |
minify | 'esbuild' | 'esbuild', 'terser', or false |
cssMinify | same as minify | CSS-specific minifier |
assetsInlineLimit | 4096 | Inline assets smaller than this (bytes) as base64 |
chunkSizeWarningLimit | 500 | Warn on chunks exceeding this size (kB) |
Code Splitting
Vite automatically splits code at dynamic import() boundaries. Each dynamic import becomes a separate chunk:
const AdminPanel = lazy(() => import('./admin/AdminPanel'))
const routes = [
{ path: '/admin', element: <AdminPanel /> },
]Manual Chunks
Control chunk grouping with manualChunks:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
},
},
},
},
});Function-Based Manual Chunks
For dynamic grouping logic:
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'vendor-react'
if (id.includes('@radix-ui')) return 'vendor-ui'
return 'vendor'
}
}Be cautious with function-based splitting: overly aggressive grouping can create circular dependencies between chunks.
Rollup Options
Pass options directly to the underlying Rollup bundler:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name].[hash].js',
chunkFileNames: 'js/[name].[hash].js',
assetFileNames: 'assets/[name].[hash][extname]',
},
treeshake: {
moduleSideEffects: false,
},
},
},
});Output File Naming
| Pattern | Description |
|---|---|
[name] | Original file/chunk name |
[hash] | Content hash for cache busting |
[extname] | File extension with leading dot |
Multi-Page Application
Specify multiple HTML entry points for multi-page builds:
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
build: {
rollupOptions: {
input: {
main: resolve(import.meta.dirname, 'index.html'),
admin: resolve(import.meta.dirname, 'admin/index.html'),
login: resolve(import.meta.dirname, 'login/index.html'),
},
},
},
});Each entry gets its own chunk graph. During dev, navigate directly to the HTML file path (e.g., /admin/index.html).
Tree-Shaking
Vite uses Rollup's tree-shaking by default. To maximize effectiveness:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
},
},
},
});Mark packages as side-effect-free in package.json:
{
"sideEffects": false
}Or specify files with side effects:
{
"sideEffects": ["*.css", "./src/polyfills.ts"]
}CSS Code Splitting
By default, CSS used by async chunks is extracted into separate files and loaded on demand. Disable to inline all CSS into JS:
build: {
cssCodeSplit: false,
}Build Analysis
Visualize bundle composition with rollup-plugin-visualizer:
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
}),
],
});Watch Mode
Rebuild on file changes (useful for library development):
export default defineConfig({
build: {
watch: {},
},
});Dependency Pre-Bundling
Vite pre-bundles dependencies with esbuild for faster dev startup. Configure explicitly when auto-detection misses dependencies:
export default defineConfig({
optimizeDeps: {
include: ['linked-package'],
exclude: ['large-esm-package'],
},
});Configuration
Config File
Vite automatically resolves vite.config.ts (or .js, .mjs, .cjs) from the project root. Use defineConfig for type safety:
import { defineConfig } from 'vite';
export default defineConfig({
root: './src',
base: '/my-app/',
plugins: [],
});Conditional Config
Pass a function to defineConfig for command/mode-aware configuration:
import { defineConfig } from 'vite';
export default defineConfig(({ command, mode }) => {
if (command === 'serve') {
return {
server: { port: 3000 },
};
}
return {
build: { sourcemap: true },
};
});command is 'serve' during dev and 'build' during production. mode defaults to 'development' for serve and 'production' for build, overridable via --mode.
Async Config
The config function can be async for dynamic imports or async setup:
import { defineConfig } from 'vite';
export default defineConfig(async ({ command, mode }) => {
const data = await fetchSomething();
return {
define: {
__DATA__: JSON.stringify(data),
},
};
});Path Aliases
Configure module resolution aliases via resolve.alias:
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@': resolve(import.meta.dirname, 'src'),
'@components': resolve(import.meta.dirname, 'src/components'),
'@utils': resolve(import.meta.dirname, 'src/utils'),
},
},
});When using TypeScript, also add matching paths in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}Define Global Constants
Replace expressions at build time with define:
import { defineConfig } from 'vite';
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify('1.0.0'),
__DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
},
});Values must be JSON-serializable or valid JS expressions. Use JSON.stringify for strings.
Shared Options Reference
| Option | Default | Purpose |
|---|---|---|
root | process.cwd() | Project root directory (where index.html lives) |
base | '/' | Public base path for assets |
publicDir | 'public' | Static assets served as-is |
cacheDir | 'node_modules/.vite' | Cache directory for pre-bundled deps |
mode | 'development' / 'production' | Overridable via CLI --mode |
logLevel | 'info' | 'info', 'warn', 'error', 'silent' |
clearScreen | true | Clear terminal on dev server start |
envDir | root | Directory to load .env files from |
envPrefix | 'VITE_' | Env variables exposed to client code |
Multi-Config with Workspaces
In monorepos, each package can have its own vite.config.ts. Shared configuration can be extracted to a function:
// packages/shared/vite-config.ts
import { type UserConfig } from 'vite';
export function createConfig(overrides: UserConfig = {}): UserConfig {
return {
resolve: {
alias: {
'@': resolve(import.meta.dirname, 'src'),
},
},
...overrides,
};
}// packages/app/vite.config.ts
import { defineConfig } from 'vite';
import { createConfig } from '../shared/vite-config';
export default defineConfig(
createConfig({
server: { port: 3000 },
}),
);Config Intellisense
For JavaScript config files, use the JSDoc type hint:
/** @type {import('vite').UserConfig} */
export default {
plugins: [],
};Or use defineConfig which provides the same type safety without JSDoc.
CSS and Assets
CSS Imports
Vite handles CSS imports natively. Imported CSS is injected via <style> tags during dev and extracted into files during build:
import './styles/global.css';CSS Modules
Files ending in .module.css are treated as CSS modules:
/* Button.module.css */
.root {
padding: 8px 16px;
border-radius: 4px;
}
.primary {
background: blue;
color: white;
}import styles from './Button.module.css';
function Button({
variant = 'primary',
}: {
variant?: 'primary' | 'secondary';
}) {
return <button className={`${styles.root} ${styles[variant]}`}>Click</button>;
}CSS Modules Config
import { defineConfig } from 'vite';
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: '[name]__[local]___[hash:base64:5]',
hashPrefix: 'prefix',
},
},
});| Option | Values | Purpose |
|---|---|---|
localsConvention | 'camelCase', 'camelCaseOnly', 'dashes', 'dashesOnly' | Class name export style |
generateScopedName | String pattern or function | Custom scoped name pattern |
hashPrefix | String | Add prefix to hash for uniqueness |
scopeBehaviour | 'global', 'local' | Default scope behavior |
PostCSS
Vite applies PostCSS automatically if a config file is detected. Supported config files: postcss.config.js, postcss.config.cjs, postcss.config.mjs, postcss.config.ts.
// postcss.config.js
export default {
plugins: {
autoprefixer: {},
'postcss-nesting': {},
},
};Alternatively, configure inline via css.postcss:
import { defineConfig } from 'vite';
import autoprefixer from 'autoprefixer';
import nesting from 'postcss-nesting';
export default defineConfig({
css: {
postcss: {
plugins: [autoprefixer(), nesting()],
},
},
});Inline config disables automatic config file detection.
CSS Preprocessors
Vite supports Sass, Less, and Stylus with zero config (install the preprocessor):
pnpm add -D sass
pnpm add -D less
pnpm add -D stylusPreprocessor Options
import { defineConfig } from 'vite';
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/variables" as *;`,
includePaths: ['./src/styles'],
api: 'modern-compiler',
},
less: {
math: 'parens-division',
modifyVars: {
'primary-color': '#1890ff',
},
},
},
},
});additionalData is prepended to every Sass/Less file. Use @use instead of @import for modern Sass.
Sass Modern API
Vite uses the Sass modern API by default. Set api: 'modern-compiler' to use the faster embedded compiler, or api: 'legacy' for legacy compatibility:
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler',
},
},
}Lightning CSS
Use Lightning CSS as an alternative CSS transformer for faster processing and built-in features (nesting, custom media queries, color functions):
import { defineConfig } from 'vite';
export default defineConfig({
css: {
transformer: 'lightningcss',
lightningcss: {
targets: {
chrome: 111,
firefox: 114,
safari: 16,
},
drafts: {
customMedia: true,
},
},
},
});When using Lightning CSS, PostCSS is bypassed. Configure browser targets via lightningcss.targets instead of Autoprefixer.
Dev Sourcemaps
Enable CSS sourcemaps during development:
css: {
devSourcemap: true,
}Static Asset Handling
Importing assets returns the resolved public URL:
import imgUrl from './assets/image.png';
// imgUrl = '/assets/image.2d8e4f.png' (with hash in production)
import workerUrl from './worker.js?worker&url';Asset URL Suffixes
| Suffix | Result |
|---|---|
| (none) | Resolved URL string |
?url | Force URL import |
?raw | Import as raw string content |
?inline | Force inline as base64 |
?worker | Import as Web Worker |
Inline Threshold
Assets smaller than assetsInlineLimit (default 4096 bytes) are inlined as base64 data URIs:
build: {
assetsInlineLimit: 8192,
}Set to 0 to disable inlining entirely.
Public Directory
Files in public/ are served at root and copied as-is during build. They are never processed or hashed:
public/
├── favicon.ico
├── robots.txt
└── og-image.pngReference in HTML or code with absolute paths:
<img src="/og-image.png" />const url = '/robots.txt';Do not import public files from JavaScript (use src/assets/ for that).
JSON Import
JSON files can be imported directly and support named imports for tree-shaking:
import data from './data.json';
import { version } from './package.json';SVG Handling
SVGs can be imported as URLs (default) or as React components with vite-plugin-svgr:
import logoUrl from './logo.svg';
import { ReactComponent as Logo } from './logo.svg?react';With vite-plugin-svgr:
import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [svgr()],
});Font Handling
Fonts in src/assets/ are processed and hashed. Fonts in public/ are copied as-is:
/* Processed and hashed */
@font-face {
font-family: 'CustomFont';
src: url('./fonts/custom.woff2') format('woff2');
}Dev Server
Basic Server Config
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
strictPort: true,
host: '0.0.0.0',
open: true,
cors: true,
},
});| Option | Default | Purpose |
|---|---|---|
port | 5173 | Dev server port |
strictPort | false | Fail if port is in use (instead of trying next) |
host | 'localhost' | Set '0.0.0.0' or true to expose on network |
open | false | Open browser on start; can be a URL string |
cors | false | Enable CORS headers |
Proxy Configuration
Forward API requests to a backend server during development:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false,
},
'/api/v2': {
target: 'http://localhost:9090',
changeOrigin: true,
},
'/socket.io': {
target: 'ws://localhost:5174',
ws: true,
rewriteWsOrigin: true,
},
'/foo': 'http://localhost:4567',
},
},
});Proxy Options
| Option | Purpose |
|---|---|
target | Backend server URL |
changeOrigin | Set Host header to target (needed for virtual hosts) |
rewrite | Transform the request path |
secure | Verify SSL certs (disable for self-signed) |
ws | Enable WebSocket proxying |
configure | Access underlying http-proxy instance for event listeners |
Advanced Proxy with Event Listeners
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
proxyReq.setHeader('X-Forwarded-For', req.socket.remoteAddress ?? '')
})
proxy.on('error', (err, req, res) => {
console.error('Proxy error:', err.message)
})
},
}RegExp Path Matching
'^/api/v[0-9]+/.*': {
target: 'http://api.example.com',
changeOrigin: true,
}HMR Configuration
Customize Hot Module Replacement behavior:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
hmr: {
protocol: 'ws',
host: 'localhost',
port: 3000,
overlay: true,
},
},
});| Option | Purpose |
|---|---|
protocol | 'ws' or 'wss' |
host | WebSocket host (useful behind reverse proxy) |
port | WebSocket port |
overlay | Show error overlay in browser |
clientPort | Override port on client side (for proxy setups) |
HMR Behind a Reverse Proxy
When running behind nginx or similar:
server: {
hmr: {
host: 'my-domain.com',
clientPort: 443,
protocol: 'wss',
},
}Client-Side HMR API
Modules can self-accept HMR updates to preserve state:
let count = 0;
export function increment() {
count++;
render();
}
if (import.meta.hot) {
if (import.meta.hot.data.count !== undefined) {
count = import.meta.hot.data.count;
}
import.meta.hot.accept((newModule) => {
if (newModule) newModule.render();
});
import.meta.hot.dispose((data) => {
data.count = count;
});
}HTTPS Dev Server
Enable HTTPS with TLS certificates:
import { defineConfig } from 'vite';
import { readFileSync } from 'node:fs';
export default defineConfig({
server: {
https: {
key: readFileSync('./certs/localhost-key.pem'),
cert: readFileSync('./certs/localhost.pem'),
},
},
});For local development, generate certs with mkcert:
mkcert -install
mkcert localhost 127.0.0.1 ::1Alternatively, use the @vitejs/plugin-basic-ssl plugin for auto-generated untrusted certs:
import { defineConfig } from 'vite';
import basicSsl from '@vitejs/plugin-basic-ssl';
export default defineConfig({
plugins: [basicSsl()],
});File System Watching
Configure the file watcher:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
watch: {
ignored: ['**/node_modules/**', '**/.git/**'],
usePolling: true,
interval: 1000,
},
},
});usePolling is needed in some environments (Docker, WSL, network filesystems) where native file watching does not work reliably.
Preview Server
The vite preview command serves the production build locally. It shares similar config options under preview:
import { defineConfig } from 'vite';
export default defineConfig({
preview: {
port: 4173,
strictPort: true,
host: '0.0.0.0',
proxy: {
'/api': 'http://localhost:8080',
},
},
});Environment Variables
.env Files
Vite loads environment variables from .env files in the envDir (project root by default):
| File | Loaded When |
|---|---|
.env | Always |
.env.local | Always, git-ignored |
.env.[mode] | Only in specified mode |
.env.[mode].local | Only in specified mode, git-ignored |
Mode-specific files take priority over generic ones. The mode defaults to 'development' for vite dev and 'production' for vite build.
Client-Side Variables
Only variables prefixed with VITE_ are exposed to client code:
# .env
VITE_API_URL=https://api.example.com
VITE_APP_TITLE=My App
SECRET_KEY=do-not-expose
DB_PASSWORD=do-not-exposeAccess in application code via import.meta.env:
const apiUrl = import.meta.env.VITE_API_URL;
const title = import.meta.env.VITE_APP_TITLE;
// import.meta.env.SECRET_KEY is undefined (no VITE_ prefix)Vite statically replaces import.meta.env.VITE_* at build time. The variable must be used with the full dot notation; destructuring does not work:
// Works
const url = import.meta.env.VITE_API_URL;
// Does NOT work (not statically analyzable)
const { VITE_API_URL } = import.meta.env;
const key = 'VITE_API_URL';
const url = import.meta.env[key];Built-in Variables
| Variable | Type | Description |
|---|---|---|
import.meta.env.MODE | string | Current mode ('development', 'production', custom) |
import.meta.env.BASE_URL | string | Base URL from base config |
import.meta.env.PROD | boolean | true in production mode |
import.meta.env.DEV | boolean | true in development mode |
import.meta.env.SSR | boolean | true when running in SSR context |
Loading Env in Config
Use loadEnv to access env variables inside vite.config.ts (where import.meta.env is not available):
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
define: {
'process.env.API_KEY': JSON.stringify(env.SECRET_KEY),
},
server: {
proxy: {
'/api': {
target: env.API_BACKEND_URL,
changeOrigin: true,
},
},
},
};
});loadEnv Parameters
loadEnv(mode: string, envDir: string, prefixes?: string | string[])| Parameter | Purpose |
|---|---|
mode | Which .env.[mode] files to load |
envDir | Directory containing .env files |
prefixes | Filter by prefix; '' loads all variables (including non-VITE_) |
The third argument defaults to 'VITE_'. Pass '' to load all env variables.
Custom Mode
Run with a custom mode for staging or testing environments:
vite build --mode stagingThis loads .env.staging and .env.staging.local, and sets import.meta.env.MODE to 'staging'.
Custom Env Prefix
Change the prefix for client-exposed variables:
import { defineConfig } from 'vite';
export default defineConfig({
envPrefix: ['VITE_', 'PUBLIC_'],
});This exposes both VITE_* and PUBLIC_* variables to client code.
TypeScript Type Declarations
Extend ImportMetaEnv for type-safe env access:
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
readonly VITE_FEATURE_FLAGS: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}Security Considerations
Never prefix secrets with VITE_. Client-side variables are embedded in the JavaScript bundle and visible to anyone:
# SAFE - only accessible in vite.config.ts via loadEnv
DATABASE_URL=postgresql://...
API_SECRET=sk-...
# EXPOSED - embedded in client bundle
VITE_PUBLIC_API_KEY=pk-...HTML Env Replacement
Environment variables are also replaced in HTML files:
<title>%VITE_APP_TITLE%</title> <link rel="icon" href="%VITE_FAVICON_URL%" />Use the %VARIABLE_NAME% syntax. Only VITE_-prefixed variables (or custom prefix) are replaced.
Library Mode
Basic Library Config
Configure Vite to build a publishable library:
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
build: {
lib: {
entry: resolve(import.meta.dirname, 'src/index.ts'),
name: 'MyLibrary',
formats: ['es', 'cjs'],
fileName: (format) => `my-library.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
},
},
},
});Output Formats
| Format | Extension | Use Case |
|---|---|---|
'es' | .mjs / .js | ESM consumers (bundlers, modern Node) |
'cjs' | .cjs / .js | CommonJS consumers (legacy Node) |
'umd' | .umd.js | Browser <script> tag and AMD/CJS |
'iife' | .iife.js | Browser <script> tag only |
name is required for UMD/IIFE formats (the global variable name).
Externalizing Dependencies
Peer dependencies must be externalized to avoid bundling them into the library:
build: {
rollupOptions: {
external: ['react', 'react-dom', 'react/jsx-runtime'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'react/jsx-runtime': 'jsxRuntime',
},
},
},
}globals maps external module names to global variable names (only needed for UMD/IIFE).
Externalizing All Dependencies
For Node.js libraries, externalize everything in dependencies:
import { defineConfig } from 'vite';
import pkg from './package.json' with { type: 'json' };
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
},
rollupOptions: {
external: [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
],
},
},
});TypeScript Declarations
Generate .d.ts files alongside the library output using vite-plugin-dts:
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [
dts({
include: ['src'],
rollupTypes: true,
}),
],
build: {
lib: {
entry: 'src/index.ts',
formats: ['es', 'cjs'],
},
},
});rollupTypes: true bundles all declarations into a single .d.ts file.
Package.json Exports
Configure package.json for dual ESM/CJS publishing:
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"main": "./dist/my-library.cjs.js",
"module": "./dist/my-library.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/my-library.es.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/my-library.cjs.js"
}
}
},
"files": ["dist"],
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}The types condition must come first in each export block.
Multiple Entry Points
Build a library with multiple entry points:
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
export default defineConfig({
build: {
lib: {
entry: {
index: resolve(import.meta.dirname, 'src/index.ts'),
utils: resolve(import.meta.dirname, 'src/utils/index.ts'),
hooks: resolve(import.meta.dirname, 'src/hooks/index.ts'),
},
formats: ['es', 'cjs'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
preserveModules: true,
preserveModulesRoot: 'src',
},
},
},
});With preserveModules, the output mirrors the source directory structure for optimal tree-shaking.
CSS in Libraries
By default, CSS is extracted to a separate file. Consumers must import it explicitly:
import 'my-library/dist/style.css';To inject CSS at runtime instead (no separate import needed), use vite-plugin-css-injected-by-js:
import { defineConfig } from 'vite';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
export default defineConfig({
plugins: [cssInjectedByJsPlugin()],
build: {
lib: {
entry: 'src/index.ts',
formats: ['es'],
},
},
});Library Build Without Minification
Libraries are typically published unminified so consumers can tree-shake and minify in their own builds:
build: {
lib: { entry: 'src/index.ts', formats: ['es'] },
minify: false,
sourcemap: true,
}Plugins
Plugin Structure
A Vite plugin is a function returning an object with a name and hook methods:
import type { Plugin } from 'vite';
export default function myPlugin(options: { debug?: boolean } = {}): Plugin {
return {
name: 'vite-plugin-my-plugin',
enforce: 'pre',
transform(code, id) {
if (!id.endsWith('.custom')) return;
return {
code: `export default ${JSON.stringify(code)}`,
map: null,
};
},
};
}Plugin Ordering
enforce | Execution Order |
|---|---|
'pre' | Before Vite core plugins |
| (none) | After Vite core plugins |
'post' | After Vite build plugins |
Conditional Application
Restrict a plugin to serve or build with apply:
export default function devOnlyPlugin(): Plugin {
return {
name: 'vite-plugin-dev-only',
apply: 'serve',
configureServer(server) {
server.middlewares.use('/debug', (req, res) => {
res.end('Debug info');
});
},
};
}apply can also be a function for fine-grained control:
apply(config, { command, mode }) {
return command === 'build' && mode !== 'test'
}Core Hooks
config
Modify user config before resolution:
config(userConfig, { command, mode }) {
return {
define: {
__PLUGIN_ENABLED__: true,
},
}
}configResolved
Access the final resolved config (read-only):
let config: ResolvedConfig
configResolved(resolvedConfig) {
config = resolvedConfig
}resolveId + load (Virtual Modules)
Virtual modules generate code at build time without filesystem files:
const VIRTUAL_ID = 'virtual:my-module';
const RESOLVED_ID = '\0virtual:my-module';
export default function virtualPlugin(): Plugin {
return {
name: 'vite-plugin-virtual',
resolveId(id) {
if (id === VIRTUAL_ID) return RESOLVED_ID;
},
load(id) {
if (id === RESOLVED_ID) {
return `export const timestamp = ${Date.now()}`;
}
},
};
}The \0 prefix is a Rollup convention that tells other plugins not to process the module.
transform
Transform module source code:
transform(code, id) {
if (!id.endsWith('.ts')) return
return {
code: code.replace(/__TIMESTAMP__/g, String(Date.now())),
map: null,
}
}configureServer
Add custom middleware to the dev server:
configureServer(server) {
server.middlewares.use('/health', (req, res) => {
res.end('OK')
})
// Return a function to add middleware AFTER Vite internals
return () => {
server.middlewares.use((req, res, next) => {
if (!res.writableEnded) {
res.statusCode = 404
res.end('Not Found')
}
})
}
}transformIndexHtml
Inject scripts/tags into the HTML entry:
transformIndexHtml: {
order: 'pre',
handler(html, ctx) {
return {
html,
tags: [
{
tag: 'script',
attrs: { type: 'module' },
children: `window.__BUILD_TIME__ = "${new Date().toISOString()}"`,
injectTo: 'head',
},
],
}
},
}handleHotUpdate
Control HMR behavior for file changes:
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.config.json')) {
ctx.server.ws.send({
type: 'custom',
event: 'config-update',
data: { file: ctx.file },
})
return []
}
}Returning an empty array prevents default HMR for that file.
Popular Plugins
| Plugin | Purpose |
|---|---|
@vitejs/plugin-react | React Fast Refresh + JSX |
@vitejs/plugin-react-swc | React with SWC (faster builds) |
@vitejs/plugin-vue | Vue 3 SFC support |
vite-plugin-dts | Generate .d.ts for library mode |
vite-plugin-pwa | Progressive Web App support |
vite-plugin-svgr | Import SVGs as React components |
@sveltejs/vite-plugin-svelte | Svelte support |
vite-tsconfig-paths | Resolve TS path aliases from tsconfig |
unplugin-auto-import | Auto-import APIs (Vue, React, etc.) |
vite-plugin-checker | TypeScript/ESLint checking in worker thread |
Using Plugins
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import checker from 'vite-plugin-checker';
export default defineConfig({
plugins: [react(), checker({ typescript: true })],
});Plugins are applied in order. Use enforce to control relative ordering with Vite internals.
SSR Configuration
SSR Build Config
Configure Vite for SSR output:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
ssr: true,
rollupOptions: {
input: './src/entry-server.ts',
},
},
ssr: {
external: ['express'],
noExternal: ['my-ui-library'],
},
});SSR Options
| Option | Purpose |
|---|---|
ssr.external | Dependencies to externalize (not bundled, resolved at runtime) |
ssr.noExternal | Dependencies to bundle (useful for ESM-only packages or packages with CSS) |
ssr.target | SSR target environment: 'node' (default) or 'webworker' |
By default, Vite externalizes all node_modules during SSR builds. Use noExternal for packages that need transformation (e.g., packages shipping uncompiled CSS or ESM-only code).
Express Integration (Development)
Set up a dev server with SSR rendering:
// server.ts
import express from 'express';
import fs from 'node:fs';
import { createServer as createViteServer } from 'vite';
async function createServer() {
const app = express();
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom',
});
app.use(vite.middlewares);
app.use('*', async (req, res) => {
const url = req.originalUrl;
try {
let template = fs.readFileSync('./index.html', 'utf-8');
template = await vite.transformIndexHtml(url, template);
const { render } = await vite.ssrLoadModule('/src/entry-server.ts');
const appHtml = await render(url);
const html = template.replace('<!--app-html-->', appHtml);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
console.error(e);
res.status(500).end((e as Error).message);
}
});
app.listen(3000);
}
createServer();Key APIs
| API | Purpose |
|---|---|
createServer({ server: { middlewareMode: true } }) | Run Vite as middleware (no built-in HTTP server) |
vite.middlewares | Connect middleware instance for HMR and static files |
vite.transformIndexHtml(url, html) | Apply Vite HTML transforms (plugin injections) |
vite.ssrLoadModule(path) | Load and execute a module in SSR context with HMR |
vite.ssrFixStacktrace(error) | Fix stack traces to map to original source |
Entry Files
Client Entry
// src/entry-client.ts
import { hydrateRoot } from 'react-dom/client'
import { App } from './App'
hydrateRoot(document.getElementById('app')!, <App />)Server Entry
// src/entry-server.ts
import { renderToString } from 'react-dom/server'
import { App } from './App'
export function render(url: string) {
return renderToString(<App />)
}Production SSR Server
In production, serve the pre-built SSR bundle without Vite:
// server-prod.ts
import express from 'express';
import fs from 'node:fs';
import { resolve } from 'node:path';
const app = express();
const distPath = resolve(import.meta.dirname, 'dist/client');
app.use(express.static(distPath, { index: false }));
const template = fs.readFileSync(resolve(distPath, 'index.html'), 'utf-8');
const { render } = await import('./dist/server/entry-server.js');
app.use('*', async (req, res) => {
const appHtml = await render(req.originalUrl);
const html = template.replace('<!--app-html-->', appHtml);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
});
app.listen(3000);Streaming SSR
For React 18+ streaming with renderToPipeableStream:
// src/entry-server.ts
import { renderToPipeableStream } from 'react-dom/server'
import { App } from './App'
export function render(url: string, res: import('express').Response) {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
res.setHeader('Content-Type', 'text/html')
pipe(res)
},
onError(err) {
console.error(err)
},
})
}Build Scripts
Typical package.json scripts for SSR:
{
"scripts": {
"dev": "node server.ts",
"build": "pnpm build:client && pnpm build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --outDir dist/server --ssr src/entry-server.ts",
"preview": "node server-prod.ts"
}
}Conditional Logic for SSR vs Client
Use import.meta.env.SSR to branch logic:
if (import.meta.env.SSR) {
// Server-only code (tree-shaken from client bundle)
} else {
// Client-only code (tree-shaken from SSR bundle)
}