vite-patterns

Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization.…

INSTALLATION
npx skills add https://github.com/affaan-m/everything-claude-code --skill vite-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Vite Patterns

Build tool and dev server patterns for Vite 8+ projects. Covers configuration, environment variables, proxy setup, library mode, dependency pre-bundling, and common production pitfalls.

When to Use

  • Configuring vite.config.ts or vite.config.js
  • Setting up environment variables or .env files
  • Configuring dev server proxy for API backends
  • Optimizing build output (chunks, minification, assets)
  • Publishing libraries with build.lib
  • Troubleshooting dependency pre-bundling or CJS/ESM interop
  • Debugging HMR, dev server, or build errors
  • Choosing or ordering Vite plugins

How It Works

  • Dev mode serves source files as native ESM — no bundling. Transforms happen on-demand per module request, which is why cold starts are fast and HMR is precise.
  • Build mode uses Rolldown (v7+) or Rollup (v5–v6) to bundle the app for production with tree-shaking, code-splitting, and Oxc-based minification.
  • Dependency pre-bundling converts CJS/UMD deps to ESM once via esbuild and caches the result under node_modules/.vite, so subsequent starts skip the work.
  • Plugins share a unified interface across dev and build — the same plugin object works for both the dev server's on-demand transforms and the production pipeline.
  • Environment variables are statically inlined at build time. VITE_-prefixed vars become public constants in the bundle; everything unprefixed is invisible to client code.

Examples

Config Structure

#### Basic Config

// vite.config.ts

import { defineConfig } from 'vite'

import react from '@vitejs/plugin-react'

export default defineConfig({

  plugins: [react()],

  resolve: {

    alias: { '@': new URL('./src', import.meta.url).pathname },

  },

})

#### Conditional Config

// vite.config.ts

import { defineConfig, loadEnv } from 'vite'

import react from '@vitejs/plugin-react'

export default defineConfig(({ command, mode }) => {

  const env = loadEnv(mode, process.cwd())   // VITE_ prefixed only (safe)

  return {

    plugins: [react()],

    server: command === 'serve' ? { port: 3000 } : undefined,

    define: {

      __API_URL__: JSON.stringify(env.VITE_API_URL),

    },

  }

})

#### Key Config Options

Key

Default

Description

root

'.'

Project root (where index.html lives)

base

'/'

Public base path for deployed assets

envPrefix

'VITE_'

Prefix for client-exposed env vars

build.outDir

'dist'

Output directory

build.minify

'oxc'

Minifier ('oxc', 'terser', or false)

build.sourcemap

false

true, 'inline', or 'hidden'

Plugins

#### Essential Plugins

Most plugin needs are covered by a handful of well-maintained packages. Reach for these before writing your own.

Plugin

Purpose

When to use

@vitejs/plugin-react-swc

React HMR + Fast Refresh via SWC

Default for React apps (faster than Babel variant)

@vitejs/plugin-react

React HMR + Fast Refresh via Babel

Only if you need Babel plugins (emotion, MobX decorators)

@vitejs/plugin-vue

Vue 3 SFC support

Vue apps

vite-plugin-checker

Runs tsc + ESLint in worker thread with HMR overlay

Any TypeScript app — Vite does NOT type-check during vite build

vite-tsconfig-paths

Honors tsconfig.json paths aliases

Any time you already have aliases in tsconfig.json

vite-plugin-dts

Emits .d.ts files in library mode

Publishing TypeScript libraries

vite-plugin-svgr

Imports SVGs as React components

React apps using SVGs as components

rollup-plugin-visualizer

Bundle treemap/sunburst report

Periodic bundle size audits (use enforce: 'post')

vite-plugin-pwa

Zero-config PWA + Workbox

Offline-capable apps

Critical callout: vite build transpiles but does NOT type-check. Type errors silently ship to production unless you add vite-plugin-checker or run tsc --noEmit in CI.

#### Authoring Custom Plugins

Authoring is rare — most needs are covered by existing plugins. When you do need one, start inline in vite.config.ts and only extract if reused.

// vite.config.ts — minimal inline plugin

function myPlugin(): Plugin {

  return {

    name: 'my-plugin',                       // required, must be unique

    enforce: 'pre',                           // 'pre' | 'post' (optional)

    apply: 'build',                           // 'build' | 'serve' (optional)

    transform(code, id) {

      if (!id.endsWith('.custom')) return

      return { code: transformCustom(code), map: null }

    },

  }

}

Key hooks: transform (modify source), resolveId + load (virtual modules), transformIndexHtml (inject into HTML), configureServer (add dev middleware), hotUpdate (custom HMR — replaces deprecated handleHotUpdate in v7+).

Virtual modules use the \0 prefix convention — resolveId returns '\0virtual:my-id' so other plugins skip it. User code imports 'virtual:my-id'.

For full plugin API, see vite.dev/guide/api-plugin. Use vite-plugin-inspect during development to debug the transform pipeline.

HMR API

Framework plugins (@vitejs/plugin-react, @vitejs/plugin-vue, etc.) handle HMR automatically. Reach for import.meta.hot directly only when building custom state stores, dev tools, or framework-agnostic utilities that need to persist state across updates.

// src/store.ts — manual HMR for a vanilla module

if (import.meta.hot) {

  // Persist state across updates (must MUTATE, never reassign .data)

  import.meta.hot.data.count = import.meta.hot.data.count ?? 0

  // Cleanup side effects before module is replaced

  import.meta.hot.dispose((data) => clearInterval(data.intervalId))

  // Accept this module's own updates

  import.meta.hot.accept()

}

All import.meta.hot code is tree-shaken out of production builds — no guard removal needed.

Environment Variables

Vite loads .env, .env.local, .env.[mode], and .env.[mode].local in that order (later overrides earlier); *.local files are gitignored and meant for local secrets.

#### Client-Side Access

Only VITE_-prefixed vars are exposed to client code:

import.meta.env.VITE_API_URL   // string

import.meta.env.MODE            // 'development' | 'production' | custom

import.meta.env.BASE_URL        // base config value

import.meta.env.DEV             // boolean

import.meta.env.PROD            // boolean

import.meta.env.SSR             // boolean

#### Using Env in Config

// vite.config.ts

import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => {

  const env = loadEnv(mode, process.cwd())          // VITE_ prefixed only (safe)

  return {

    define: {

      __API_URL__: JSON.stringify(env.VITE_API_URL),

    },

  }

})

Security

#### VITE_ Prefix is NOT a Security Boundary

Any variable prefixed with VITE_ is statically inlined into the client bundle at build time. Minification, base64 encoding, and disabling source maps do NOT hide it. A determined attacker can extract any VITE_ var from the shipped JavaScript.

Rule: Only public values (API URLs, feature flags, public keys) go in VITE_ vars. Secrets (API tokens, database URLs, private keys) MUST live server-side behind an API or serverless function.

#### The loadEnv('') Trap

// BAD: passing '' as the third arg loads ALL env vars — including server secrets —

// and makes them available to inline into client code via `define`.

const env = loadEnv(mode, process.cwd(), '')

// GOOD: explicit prefix list

const env = loadEnv(mode, process.cwd(), ['VITE_', 'APP_'])

#### Source Maps in Production

Production source maps leak your original source code. Disable them unless you upload to an error tracker (Sentry, Bugsnag) and delete locally afterward:

build: {

  sourcemap: false,                                  // default — keep it this way

}

#### .gitignore Checklist

  • .env.local, .env.*.local — local secret overrides
  • dist/ — build output
  • node_modules/.vite — pre-bundle cache (stale entries cause phantom errors)

Server Proxy

// vite.config.ts — server.proxy

server: {

  proxy: {

    '/foo': 'http://localhost:4567',                    // string shorthand

    '/api': {

      target: 'http://localhost:8080',

      changeOrigin: true,                               // needed for virtual-hosted backends

      rewrite: (path) => path.replace(/^\/api/, ''),

    },

  },

}

For WebSocket proxying, add ws: true to the route config.

Build Optimization

#### Manual Chunks

// vite.config.ts — build.rolldownOptions

build: {

  rolldownOptions: {

    output: {

      // Object form: group specific packages

      manualChunks: {

        'react-vendor': ['react', 'react-dom'],

        'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-popover'],

      },

    },

  },

}
// Function form: split by heuristic

manualChunks(id) {

  if (id.includes('node_modules/react')) return 'react-vendor'

  if (id.includes('node_modules')) return 'vendor'

}

Performance

#### Avoid Barrel Files

Barrel files (index.ts re-exporting everything from a directory) force Vite to load every re-exported file even when you import a single symbol. This is the #1 dev-server slowdown flagged by the official docs.

// BAD — importing one util forces Vite to load the whole barrel

import { slash } from '@/utils'

// GOOD — direct import, only the one file is loaded

import { slash } from '@/utils/slash'

#### Be Explicit with Import Extensions

Each implicit extension forces up to 6 filesystem checks via resolve.extensions. In large codebases, this adds up.

// BAD

import Component from './Component'

// GOOD

import Component from './Component.tsx'

Narrow tsconfig.json allowImportingTsExtensions + resolve.extensions to only the extensions you actually use.

#### Warm-Up Hot-Path Routes

server.warmup.clientFiles pre-transforms known hot entries before the browser requests them — eliminating the cold-load request waterfall on large apps.

// vite.config.ts

server: {

  warmup: {

    clientFiles: ['./src/main.tsx', './src/routes/**/*.tsx'],

  },

}

#### Profiling Slow Dev Servers

When vite dev feels slow, start with vite --profile, interact with the app, then press p+enter to save a .cpuprofile. Load it in Speedscope to find which plugins are eating time — usually buildStart, config, or configResolved hooks in community plugins.

Library Mode

When publishing an npm package, use build.lib. Two footguns matter more than config detail:

  • Types are not emitted — add vite-plugin-dts or run tsc --emitDeclarationOnly separately.
  • Peer dependencies MUST be externalized — unlisted peers get bundled into your library, causing duplicate-runtime errors in consumers.
// vite.config.ts

build: {

  lib: {

    entry: 'src/index.ts',

    formats: ['es', 'cjs'],

    fileName: (format) => `my-lib.${format}.js`,

  },

  rolldownOptions: {

    external: ['react', 'react-dom', 'react/jsx-runtime'],  // every peer dep

  },

}

SSR Externals

Bare createServer({ middlewareMode: true }) setups are framework-author territory. Most apps should use Nuxt, Remix, SvelteKit, Astro, or TanStack Start instead. What you will tweak as a framework user is the externals config when deps break in SSR:

// vite.config.ts — ssr options

ssr: {

  external: ['node-native-package'],           // keep as require() in SSR bundle

  noExternal: ['esm-only-package'],            // force-bundle into SSR output (fixes most SSR errors)

  target: 'node',                              // 'node' or 'webworker'

}

Dependency Pre-Bundling

Vite pre-bundles dependencies to convert CJS/UMD to ESM and reduce request count.

// vite.config.ts — optimizeDeps

optimizeDeps: {

  include: [

    'lodash-es',                              // force pre-bundle known heavy deps

    'cjs-package',                            // CJS deps that cause interop issues

    'deep-lib/components/**',                 // glob for deep imports

  ],

  exclude: ['local-esm-package'],             // must be valid ESM if excluded

  force: true,                                // ignore cache, re-optimize (temporary debugging)

}

Common Pitfalls

#### Dev Does Not Match Build

Dev uses esbuild/Rolldown for transforms; build uses Rolldown for bundling. CJS libraries can behave differently between the two. Always verify with vite build && vite preview before deploying.

#### Stale Chunks After Deployment

New builds produce new chunk hashes. Users with active sessions request old filenames that no longer exist. Vite has no built-in solution. Mitigations:

  • Keep old dist/assets/ files live for a deployment window
  • Catch dynamic import errors in your router and force a page reload

#### Docker and Containers

Vite binds to localhost by default, which is unreachable from outside a container:

// vite.config.ts — Docker/container setup

server: {

  host: true,                                  // bind 0.0.0.0

  hmr: { clientPort: 3000 },                   // if behind a reverse proxy

}

#### Monorepo File Access

Vite restricts file serving to the project root. Packages outside root are blocked:

// vite.config.ts — monorepo file access

server: {

  fs: {

    allow: ['..'],                             // allow parent directory (workspace root)

  },

}

Anti-Patterns

// BAD: Setting envPrefix to '' exposes ALL env vars (including secrets) to the client

envPrefix: ''

// BAD: Assuming require() works in application source code — Vite is ESM-first

const lib = require('some-lib')                // use import instead

// BAD: Splitting every node_module into its own chunk — creates hundreds of tiny files

manualChunks(id) {

  if (id.includes('node_modules')) {

    return id.split('node_modules/')[1].split('/')[0]   // one chunk per package

  }

}

// BAD: Not externalizing peer deps in library mode — causes duplicate runtime errors

// build.lib without rolldownOptions.external

// BAD: Using deprecated esbuild minifier

build: { minify: 'esbuild' }                  // use 'oxc' (default) or 'terser'

// BAD: Mutating import.meta.hot.data by reassignment

import.meta.hot.data = { count: 0 }           // WRONG: must mutate properties, not reassign

import.meta.hot.data.count = 0                 // CORRECT

Process anti-patterns:

  • **vite preview is NOT a production server** — it is a smoke test for the built bundle. Deploy dist/ to a real static host (NGINX, Cloudflare Pages, Vercel static) or use a multi-stage Dockerfile.
  • **Expecting vite build to type-check** — it only transpiles. Type errors silently ship to production. Add vite-plugin-checker or run tsc --noEmit in CI.
  • **Shipping @vitejs/plugin-legacy by default** — it bloats bundles ~40%, breaks source-map bundle analyzers, and is unnecessary for the 95%+ of users on modern browsers. Gate it on real analytics, not assumption.
  • **Hand-rolling 30+ resolve.alias entries that duplicate tsconfig.json paths** — use vite-tsconfig-paths instead. Observed in Excalidraw and PostHog; avoid in new projects.
  • **Leaving stale node_modules/.vite after dep changes** — pre-bundle cache causes phantom errors. Clear it when switching branches or after patching deps.

Quick Reference

Pattern

When to Use

defineConfig

Always — provides type inference

loadEnv(mode, root, ['VITE_'])

Access env vars in config (explicit prefix)

vite-plugin-checker

Any TypeScript app (fills the type-check gap)

vite-tsconfig-paths

Instead of hand-rolled resolve.alias

optimizeDeps.include

CJS deps causing interop issues

server.proxy

Route API requests to backend in dev

server.host: true

Docker, containers, remote access

server.warmup.clientFiles

Pre-transform hot-path routes

build.lib + external

Publishing npm packages

manualChunks (object)

Vendor bundle splitting

vite --profile

Debug slow dev server

vite build && vite preview

Smoke-test prod bundle locally (NOT a prod server)

Related Skills

  • frontend-patterns — React component patterns
  • docker-patterns — containerized dev with Vite
  • nextjs-turbopack — alternative bundler for Next.js
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card