Claude Code with Vite: Config, Plugins, Build Optimisation
Why Vite needs a project-specific CLAUDE.md
Vite sits underneath almost every modern frontend project. React, Vue, Svelte, Solid, Lit, Astro, Qwik, and the long tail of bundler-shy frameworks all use Vite as the dev server and build tool. The framework on top changes; the config patterns underneath stay consistent.
Claude Code understands Vite. It knows defineConfig, the plugin API, dev server options, build output structure, env variables, and library mode. What it does not know is your project: which plugins you use, how aliases are set up, what your proxy looks like, and which framework adapter is doing what.
Without a project-specific CLAUDE.md, Claude generates Vite config that imports plugins from the wrong package, breaks HMR by configuring the dev server incorrectly, hardcodes paths that should be aliases, and produces build output that fails in production. The bug is rarely loud. The dev server boots, HMR mostly works, the build mostly succeeds, and then you ship something that fails to chunk vendors correctly.
This guide covers the CLAUDE.md rules and config patterns that make Claude Code reliable for Vite. If you are new, the Claude Code setup guide covers installation and authentication first.
The Vite CLAUDE.md template
The CLAUDE.md at your project root is read before every Claude Code session. For Vite, it needs to answer: which version, which framework adapter, which plugins are active, what the alias structure is, how the dev server is configured, and what the production build is supposed to look like.
# Vite project rules
## Stack
- Vite: 6.x (rolldown-vite is opt-in, not the default)
- Framework: @vitejs/plugin-react-swc (React 19) | @vitejs/plugin-vue (Vue 3) | @sveltejs/vite-plugin-svelte
- TypeScript: 5.6.x with strict mode
- Node: 20.x (engines pinned in package.json)
- Package manager: pnpm 9.x (do not switch to npm or yarn)
- CSS: Tailwind v4 via @tailwindcss/vite plugin (no PostCSS config)
- Test runner: Vitest 2.x
## Project structure
- src/: application source
- src/lib/: shared utilities (alias: $lib)
- src/components/: UI components (alias: $components)
- public/: static assets served as-is from /
- vite.config.ts: single config file, no environment-specific configs
- vitest.config.ts: imports from vite.config.ts, extends test settings only
## Vite syntax rules
- Use defineConfig with TypeScript, never plain object export
- Plugin order matters: framework plugin first, then css plugin, then user plugins
- Always use path aliases via resolve.alias, never relative imports across feature folders
- Env vars: VITE_ prefix only, accessed via import.meta.env, never process.env
- Use import.meta.glob for dynamic imports, not require.context
## Running the project
- Dev: `pnpm dev` (port 5173, do not change without updating proxy configs)
- Build: `pnpm build` (output to dist/)
- Preview: `pnpm preview` (production preview on port 4173)
- Test: `pnpm test` (Vitest, watch mode)
- Test (CI): `pnpm test:ci` (single run, with coverage)
## Hard rules
- NEVER edit vite.config.ts and package.json in the same change without restarting the dev server
- NEVER add a plugin without checking it is a Vite plugin, not a Rollup plugin (different API)
- NEVER access process.env in client code, always import.meta.env with VITE_ prefix
- NEVER hardcode the dev port in source code, read from import.meta.env.MODE if needed
- NEVER mix CommonJS and ESM imports in vite.config.ts (use ESM only)
- Build output must be deterministic: no Date.now() in chunk names, no random hashes outside content hashes
Three rules in this template prevent the most common Claude Code failures.
The plugin API rule catches most teams. Vite plugins extend Rollup plugins with hooks like configureServer, transformIndexHtml, and handleHotUpdate. A Rollup plugin works in production builds but does nothing in the dev server. Without the explicit rule, Claude will pull in a Rollup plugin and slot it into the plugins array, and the dev server will silently ignore it.
The env var rule covers bugs that survive into production. process.env is undefined in browser code. Vite replaces import.meta.env.VITE_* at build time. Claude defaults to process.env patterns from Node.js training data. The VITE_ prefix is also a security boundary.
The port rule is small but load-bearing. Frontend projects have a dev server proxy pointing to a backend on a known port, or a backend with CORS rules pointing to 5173. If Claude changes the dev port, both halves of the stack break.
For TypeScript-focused projects, the Claude Code TypeScript guide covers tsconfig strictness rules and type generation patterns.
vite.config.ts patterns that work
Once CLAUDE.md is in place, give Claude a real config to extend rather than generate from scratch. A reference config lets Claude pattern-match against your actual stack instead of generic templates.
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [
react(),
tailwindcss(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'$lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
'$components': fileURLToPath(new URL('./src/components', import.meta.url)),
},
},
server: {
port: 5173,
strictPort: true,
host: '127.0.0.1',
proxy: {
'/api': {
target: env.VITE_API_URL ?? 'http://localhost:8787',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
target: 'es2022',
sourcemap: command === 'build' ? 'hidden' : true,
cssCodeSplit: true,
reportCompressedSize: false,
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
define: {
__APP_VERSION__: JSON.stringify(env.npm_package_version),
},
}
})
A few elements are worth highlighting because Claude reproduces them once they are in your repo.
The defineConfig callback form (rather than a plain object) gives access to mode and command. This is how you write config that behaves differently in dev, build, and serve without maintaining separate files. The loadEnv call reads .env files for the current mode, letting you drive proxy targets from env without hardcoding URLs. The VITE_ prefix is not required inside the config (only for client exposure), so this can read backend-only secrets.
strictPort: true makes Vite fail if 5173 is in use rather than silently switching to 5174. Any port mismatch becomes a loud failure.
The manualChunks config is where most teams hand-tune their build. Default Vite chunking is reasonable, but splitting React or your router into separate chunks materially improves first-paint caching across deployments. Claude generates this correctly when an example is present but skips it when generating from scratch.
For environment variables specifically, the Claude Code environment variables guide covers .env management and secret handling.
Plugin ecosystem: Vite plugins, not Rollup plugins
Vite's plugin system is a superset of Rollup's. Every Rollup plugin works in production builds. Only Vite plugins work in the dev server. Claude Code regularly gets this wrong without explicit guidance.
The signal is in the import path. Vite plugins live in vite-plugin-* or @vitejs/plugin-* packages and implement Vite-specific hooks. Rollup plugins live in @rollup/plugin-* packages and only implement Rollup hooks.
Add a plugin reference section to your CLAUDE.md:
## Plugin reference
### Active Vite plugins
- @vitejs/plugin-react-swc: React fast-refresh and JSX transform via SWC
- @tailwindcss/vite: Tailwind v4 (no PostCSS config needed)
- vite-plugin-pwa: Service worker generation for PWA mode
- vite-tsconfig-paths: Reads tsconfig paths and adds them as aliases
### Active Rollup plugins (build only)
- @rollup/plugin-visualizer: Bundle analysis (only loaded in `pnpm build:analyze`)
### Plugin order rule
1. Framework plugin (react, vue, svelte) goes first
2. CSS plugin (@tailwindcss/vite) goes second
3. User-provided plugins go third
4. Bundle analysis (visualizer) goes last, conditional on env
### When adding a new plugin
- Verify it is in @vitejs/* or vite-plugin-*. If not, check it actually has a Vite hook
- Add to the plugins array in the position above that matches its category
- Add to this list with a one-line description
Plugin order matters because Vite runs plugins sequentially through transform hooks. A CSS plugin before the framework plugin sees raw component files; one after sees transformed output. Wrong order produces hard-to-debug issues that look like plugin bugs but are order bugs.
The popular plugins worth knowing in 2026: @vitejs/plugin-react-swc is the standard React plugin (faster than Babel-based). @tailwindcss/vite is the Tailwind v4 integration that replaces PostCSS config. vite-plugin-pwa handles service worker generation. vite-tsconfig-paths wires tsconfig.json paths as Vite aliases. vite-plugin-checker runs TypeScript and ESLint as a separate process during dev, surfacing errors in an overlay without blocking HMR.
Framework-specific guides cover the plugin choices for each setup: Claude Code with React, Claude Code with Vue, Claude Code with Svelte, and Claude Code with Astro.
Dev server and HMR rules
HMR is what makes Vite feel fast. It is also where bad config produces silent failures: HMR works in some files but not others, full reloads happen instead of module swaps, and certain edits cascade across unrelated modules. Most of these are config issues, not Vite bugs.
Add an HMR rules section to your CLAUDE.md:
## HMR rules
- DO NOT add a top-level if (import.meta.hot) block in modules that should fast-refresh normally
- DO NOT export non-component values (constants, hooks) from a file that exports React components. Split into separate files
- DO NOT mutate module-scope state outside React state hooks (breaks HMR boundaries)
- File-level HMR breakage signals: full page reload on edit, "[vite] hmr invalidate" warnings, lost component state on save
- When HMR misbehaves, check the dev server logs first. Vite logs the file that broke the HMR boundary
- Acceptable full-reload triggers: vite.config.ts edits, .env file edits, package.json dependency changes
The constants-with-components rule is the most common HMR failure in React projects. React Refresh can only fast-refresh files where every export is a component. Export a constant, a hook, or a utility alongside components and React Refresh falls back to a full module reload, losing component state. The fix is structural: utilities in lib/, hooks in hooks/, components in components/, with component files exporting only components.
The proxy configuration is the other common dev server issue. If your backend is on a different origin and you fetch from /api/* without a proxy, you get CORS errors in dev that you never see in production. The proxy in the config above handles this transparently, and Claude will replicate the pattern for new endpoints.
For dev servers behind a tunnel or running remotely, host: '0.0.0.0' and hmr.clientPort handle the case where the browser connects on a different port than the server binds to. This comes up in Codespaces, Gitpod, and any setup proxying through a development URL.
Build optimisation: chunking, assets, and library mode
The default Vite production build is good. Optimising it requires understanding three areas: chunk splitting, asset processing, and whether you are building an application or a library.
Add a build conventions section to your CLAUDE.md:
## Build conventions
### Chunking strategy
- Vendor chunks: separate React/Vue/Svelte runtime from app code
- Route chunks: lazy-load route components via dynamic import()
- Heavy library chunks: separate chart libs, editor libs, PDF libs into their own chunks
- Use rollupOptions.output.manualChunks for explicit control
### Asset handling
- Images under 4KB are inlined as base64 (default Vite behaviour)
- Larger images go to dist/assets/ with content hash
- SVG: import as React component via vite-plugin-svgr, or as URL via ?url suffix
- Fonts: place in public/fonts/, reference by absolute /fonts/ path
- Worker files: use the new Worker(new URL('./worker.ts', import.meta.url)) syntax
### Build modes
- Production: pnpm build (default, minified, sourcemap hidden)
- Analysis: pnpm build:analyze (adds visualizer plugin, opens stats.html)
- Library: pnpm build:lib (uses vite.config.lib.ts, library mode)
### Output structure
- dist/index.html: app entry
- dist/assets/[name]-[hash].js: code chunks
- dist/assets/[name]-[hash].css: CSS chunks
- dist/assets/[name]-[hash].[ext]: other assets
- Never write outside dist/ in a build
The chunking strategy in the example splits React into its own chunk so React updates do not invalidate the app code chunk. For applications with a router, lazy-loading route components via import() makes Vite generate a separate chunk per route, so users only download code for the page they visit.
Library mode is a different config entirely. You build to dist/ as ESM, CJS, or UMD, mark peer dependencies as external, and generate type declarations alongside JS output:
// vite.config.lib.ts
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
import { resolve } from 'node:path'
export default defineConfig({
plugins: [
dts({
insertTypesEntry: true,
include: ['src/**/*'],
}),
],
build: {
lib: {
entry: resolve(__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',
},
},
},
},
})
The external array is the part Claude Code most often gets wrong. If your library imports React, you do not want to bundle React into your output. Consumers should provide their own React. Marking react and react-dom as external tells Rollup to leave the imports as-is, so the consumer's bundler resolves them. Without this, you ship a library that bundles its own React copy, and consumers get duplicate React runtimes that break hooks.
The vite-plugin-dts plugin generates .d.ts files alongside JavaScript output. This is mandatory for any TypeScript library used from TypeScript consumers. Claude will skip type generation if it is not in CLAUDE.md, producing a library that works at runtime but has no type information.
For platform-specific shipping, the Claude Code deployment guide and Claude Code with Vercel cover build adjustments that pair with these Vite settings.
Permission hooks for Vite projects
Vite projects have a small set of safe commands and a small set that should be gated. The dev server, build, and tests are safe. Anything that wipes node_modules, regenerates pnpm-lock.yaml, or pushes to npm is destructive and should require approval.
In .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(pnpm dev*)",
"Bash(pnpm build*)",
"Bash(pnpm preview*)",
"Bash(pnpm test*)",
"Bash(pnpm typecheck*)",
"Bash(pnpm lint*)"
],
"deny": [
"Bash(rm -rf node_modules*)",
"Bash(pnpm install --frozen-lockfile=false*)",
"Bash(pnpm publish*)",
"Bash(pnpm version*)",
"Bash(rm -rf dist*)"
]
}
}
The pnpm install --frozen-lockfile=false deny prevents the most common accidental damage. A regular pnpm install respects the lockfile. Adding --frozen-lockfile=false regenerates it, quietly upgrading transitive dependencies. Gating this means Claude surfaces the change for approval rather than silently rebuilding the dep tree.
The pnpm publish deny is a safety net for library projects: no automated session pushes a release.
For permission hook patterns across project types, the Claude Code best practices guide covers the principles regardless of build tool.
What Claude Code handles well and where to review
Claude Code performs reliably in several Vite areas. Plugin configuration is correct when the plugin reference is in CLAUDE.md. Path aliases, env handling, and proxy are generated correctly with examples in the repo. Build output matches the existing manualChunks pattern. Library mode is accurate when vite-plugin-dts and the external/globals pattern are documented.
Three areas warrant manual review. The first is HMR-sensitive component organisation: Claude will sometimes export a hook from a component file when it seems close enough, which silently breaks fast-refresh. The second is plugin order changes: if Claude adds a plugin in the middle of the array rather than at the documented position, the build succeeds while the dev experience subtly shifts. The third is manualChunks updates: adding a new heavy dependency without splitting it is invisible until you ship. Running pnpm build:analyze after dependency changes catches this.
For Tailwind-specific patterns, Claude Code with Tailwind covers the v4 conventions for @tailwindcss/vite.
Hard rules summary
The patterns above reduce to a list of mandatory rules that belong at the top of every Vite CLAUDE.md:
- ESM only in
vite.config.ts. No CommonJS imports. - Vite plugins are not Rollup plugins. Verify package origin before adding.
import.meta.envwithVITE_prefix for client-exposed env vars. Neverprocess.env.- Plugin order: framework, CSS, user plugins, analysis.
- Path aliases over relative imports across feature folders.
- Component files export only components. Hooks, constants, utilities live elsewhere.
strictPort: truein dev server config.- Library mode uses
vite-plugin-dtsand externals for peer dependencies. - Deny
pnpm install --frozen-lockfile=false,pnpm publish, andrm -rfonnode_modulesordist. - Run
pnpm build:analyzeafter any dependency change that could affect bundle size.
These rules prevent the most common Vite failures in Claude Code sessions. They produce a dev server that boots reliably, HMR that works on every edit, a production build that is deterministic, and a bundle shaped the way you intend.
The framework on top changes the specifics, but the underlying Vite config patterns are stable across React, Vue, Svelte, and the rest of the modern frontend ecosystem. Claudify includes a Vite-specific CLAUDE.md template pre-configured for Vite 6, the major framework plugins, and the build optimisation patterns above. For the broader role of CLAUDE.md across project types, CLAUDE.md explained covers how the file is read and how to structure it for any stack.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify