agentskills.codes
VI

vite-patterns

Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization. Activate when working with vite.config.ts, Vite plugins, or Vite-based projects.

Install

mkdir -p .claude/skills/vite-patterns && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16404" && unzip -o skill.zip -d .claude/skills/vite-patterns && rm skill.zip

Installs to .claude/skills/vite-patterns

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Vite build tool patterns including config, plugins, HMR, env variables, proxy setup, SSR, library mode, dependency pre-bundling, and build optimization. Activate when working with vite.config.ts, Vite plugins, or Vite-based projects.
233 chars✓ has a “when” trigger

About this skill

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

KeyDefaultDescription
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.sourcemapfalsetrue, '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.

PluginPurposeWhen to use
@vitejs/plugin-react-swcReact HMR + Fast Refresh via SWCDefault for React apps (faster than Babel variant)
@vitejs/plugin-reactReact HMR + Fast Refresh via BabelOnly if you need Babel plugins (emotion, MobX decorators)
@vitejs/plugin-vueVue 3 SFC supportVue apps
vite-plugin-checkerRuns tsc + ESLint in worker thread with HMR overlayAny TypeScript app: Vite does NOT type-check during vite build
vite-tsconfig-pathsHonors tsconfig.json paths aliasesAny time you already have aliases in tsconfig.json
vite-plugin-dtsEmits .d.ts files in library modePublishing TypeScript libraries
vite-plugin-svgrImports SVGs as React componentsReact apps using SVGs as components
rollup-plugin-visualizerBundle treemap/sunburst reportPeriodic bundle size audits (use enforce: 'post')
vite-plugin-pwaZero-config PWA + WorkboxOffline-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'
`

---

*Content truncated.*

Search skills

Search the agent skills registry