Vite Config skill for Codex

A vite.config.ts where every line earns its place. Codex checks the installed Vite major before touching options, so it writes rolldownOptions on Vite 8 and rollupOptions on older versions. It keeps secrets out of VITE_ variables, types import.meta.env, configures proxies and HMR for containers, sets up library mode with proper externals and declaration files, and proves changes with a production build, preview and a grep of the bundle.

  • Frontend
  • $vite-config
  • 203 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- vite-config

Installs to ~/.agents/skills/vite-config/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $vite-config, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and debugs vite.config.ts for apps, libraries and SSR - env files and import.meta.env, mode-aware config, aliases, dev proxy, plugins, dependency pre-bundling, chunking, library mode with types, and the Vite 8 Rolldown option renames. Use when a Vite build or dev server misbehaves, env variables come through undefined, a library needs ES output with externals, a proxy or HMR fails behind Docker, or when upgrading Vite majors.

Use it when

  • Writing or cleaning up vite.config.ts for an app
  • Env variables, aliases or the dev proxy behave differently from expected
  • Publishing a library built with Vite library mode
  • Upgrading across Vite majors or setting up Vite SSR builds

Use something else when

How to check the result

A production build and vite preview load the changed routes without console errors, and grepping dist shows only the intended VITE_ variables were inlined.

The mistake it prevents

Putting a secret in a VITE_-prefixed variable, which Vite inlines as plain text into the public bundle.

~/.agents/skills/vite-config/SKILL.md

--- name: vite-config description: Writes and debugs vite.config.ts for apps, libraries and SSR - env files and import.meta.env, mode-aware config, aliases, dev proxy, plugins,… license: MIT ---

A good Vite config is short, mode-aware, and explains every non-default line. Secrets never reach import.meta.env, dev and build resolve modules the same way, and the production bundle is checked, not assumed.

Inspect first

  1. Read AGENTS.md and package.json: package manager, scripts, "type": "module".
  2. Get the real version, it decides option names:
npx vite --version            # or: npm ls vite
node --version                # Vite 7 and 8 need Node 20.19+ or 22.12+
InstalledBundlerOptions to use
Vite 8Rolldown + Oxcbuild.rolldownOptions, worker.rolldownOptions, optimizeDeps.rolldownOptions; the old rollupOptions/esbuild* keys still work with a deprecation warning
Vite 5-7esbuild (dev) + Rollup (build)build.rollupOptions, optimizeDeps.esbuildOptions, esbuild
  1. Find every config file: vite.config.*, vitest.config.* (overrides Vite config for tests), framework wrappers (astro.config, nuxt.config, svelte.config, react-router.config, app.config) that own the Vite instance. In a framework, put Vite options where the framework expects them, not in a stray vite.config.ts.
  2. Print the resolved config when behaviour is surprising: DEBUG=vite:config npx vite build.

Baseline app config

// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';

export default defineConfig(({ command, mode }) => {
  // Third arg '' loads all vars (not just VITE_) for use INSIDE this config only.
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [react()],
    resolve: {
      alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
    },
    server: {
      port: 5173,
      strictPort: true,
      proxy: {
        '/api': { target: env.API_ORIGIN ?? 'http://localhost:8080', changeOrigin: true },
      },
    },
    build: {
      sourcemap: mode !== 'production' ? true : 'hidden',
    },
    base: command === 'build' ? env.PUBLIC_BASE ?? '/' : '/',
  };
});

Keep the alias in sync with tsconfig.json compilerOptions.paths (or use the vite-tsconfig-paths plugin so there is one source of truth).

Environment variables

  • Load order (later wins): .env, .env.local, .env.[mode], .env.[mode].local. Shell variables beat all files. .local files must be gitignored.
  • Only variables with envPrefix (default VITE_) are exposed to client code as import.meta.env.VITE_X. They are inlined into the bundle as plain text: anything with VITE_ is public. Never prefix API secrets.
  • Built-ins: import.meta.env.MODE, DEV, PROD, SSR, BASE_URL.
  • --mode staging picks .env.staging; NODE_ENV and mode are different things.
  • Values are strings: import.meta.env.VITE_FLAG === 'true', not truthiness.
  • Type them:
// src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_SENTRY_DSN?: string;
}
interface ImportMeta { readonly env: ImportMetaEnv }
  • Need the same build artifact in several environments? Do not bake URLs in at build time; fetch /config.json at startup or have the server inject a window.__CONFIG__.

define performs raw text replacement. Values must be JSON-encoded: define: { __APP_VERSION__: JSON.stringify(pkg.version) }. Prefer env vars over define.

Decisions

Symptom or needDo
Import of a CJS dependency breaks only in devAdd it to optimizeDeps.include
Linked monorepo package not updating in devEnsure it is not pre-bundled: optimizeDeps.exclude: ['@acme/ui'], and it ships ESM
Dev server in Docker or WSL, HMR deadserver.host: true, server.watch.usePolling: true (CPU cost), set server.hmr.clientPort if behind a proxy
Accessing dev server via a custom hostnameAdd it to server.allowedHosts (blocked by default since the 2025 security fix)
One vendor chunk too largeSplit by route with dynamic import() first; manual chunking second
Need process.env.X from a legacy libdefine: { 'process.env.NODE_ENV': JSON.stringify(mode) }; do not polyfill all of process
Node built-ins (Buffer, crypto) in browser codeReplace the dependency; polyfill plugins are a last resort
Serving under a sub-pathbase: '/app/', and use import.meta.env.BASE_URL for runtime URLs
Assets that must keep their filenamepublic/, referenced by absolute path, never imported

Plugins

  • Order matters; enforce: 'pre' | 'post' and apply: 'serve' | 'build' scope them.
  • Conditional plugins: plugins: [react(), mode === 'analyze' && visualizer()] (falsy entries are ignored).
  • Write a tiny inline plugin before installing one for a one-liner transform:
const injectBuildTime = (): Plugin => ({
  name: 'inject-build-time',
  transformIndexHtml: (html) =>
    html.replace('</head>', `<meta name="build-time" content="${new Date().toISOString()}"></head>`),
});

Library mode

import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import pkg from './package.json' with { type: 'json' };

const external = [
  ...Object.keys(pkg.peerDependencies ?? {}),
  ...Object.keys(pkg.dependencies ?? {}),
].map((d) => new RegExp(`^${d}(/.*)?$`));   // also externalise deep imports like react/jsx-runtime

export default defineConfig({
  plugins: [dts({ tsconfigPath: './tsconfig.build.json' })],
  build: {
    lib: {
      entry: { index: 'src/index.ts' },   // resolved from the project root
      formats: ['es'],                   // add 'cjs' only if consumers still require()
      fileName: (format, name) => `${name}.${format === 'es' ? 'js' : 'cjs'}`,
      cssFileName: 'styles',             // Vite 6+
    },
    rolldownOptions: { external },       // Vite 7 and earlier: rollupOptions
    sourcemap: true,
    emptyOutDir: true,
  },
});

Then make package.json exports point at the emitted files and types, and verify with npm pack --dry-run and a scratch consumer project. Forgetting external bundles React into the library and causes “Invalid hook call” in consumers.

SSR

  • Build twice: vite build --outDir dist/client and vite build --ssr src/entry-server.ts --outDir dist/server.
  • ssr.noExternal: ['some-esm-only-pkg'] for deps that must be transformed (CSS imports, untranspiled TS); ssr.external for heavy Node-only deps.
  • In dev, use createServer({ server: { middlewareMode: true }, appType: 'custom' }) and vite.ssrLoadModule(); in production, never import Vite.
  • Code that touches window must be guarded by import.meta.env.SSR or moved into client-only effects.

Upgrading majors

Read the migration guide for every major you cross (vite.dev/guide/migration, needs network). Typical changes: Node minimum, default build.target (Vite 7 defaults to “baseline-widely-available”), Sass modern API, rollupOptions to rolldownOptions in 8, and plugin compatibility. Upgrade framework plugins (@vitejs/plugin-react, @vitejs/plugin-vue, @sveltejs/vite-plugin-svelte) in the same step.

Gotchas

  • vite.config.ts runs in Node: import.meta.env is not populated there. Use loadEnv.
  • vite preview serves dist/ for a local smoke test; it is not a production server.
  • index.html is the entry point and lives at the project root (or root), not in public/.
  • Absolute imports like /src/x work in dev but resolve against base in build; use aliases.
  • Changing optimizeDeps sometimes needs npx vite --force to drop the stale cache in node_modules/.vite.
  • A vitest.config.ts that does not mergeConfig with the Vite config silently loses aliases and plugins in tests.

Verification

npx vite build --mode production
npx vite preview --port 4173 --strictPort    # click through the routes that changed
grep -ro "VITE_[A-Z_]*" dist | sort -u       # confirm only intended public vars are inlined
npx vite-bundle-visualizer                   # or rollup-plugin-visualizer; needs install
  • tsc --noEmit passes and env vars are typed
  • Dev and preview both load the changed routes with no console errors
  • No secret strings in dist/ (grep for known key prefixes)
  • Library: npm pack --dry-run lists only dist, types resolve in a consumer

When reporting, list each config change with a one-line reason and the before/after build output size for the main entry.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Vite Config"
  short_description: "A vite.config.ts where every line earns its place"
  default_prompt: "$vite-config import.meta.env.VITE_API_URL is undefined in production, fix our Vite setup"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem