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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- vite-config Installs to ~/.agents/skills/vite-config/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install vite-config The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project vite-config Installs to .agents/skills/vite-config/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/vite-config.zipunzip -o vite-config.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) vite-config PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The project still runs on webpack or Create React App: use webpack-to-vite
- The goal is shrinking an already working bundle: use bundle-size-reduction
- You are configuring the test runner rather than the build: use vitest-testing
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.
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
- Read
AGENTS.mdandpackage.json: package manager, scripts,"type": "module". - 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+
| Installed | Bundler | Options to use |
|---|---|---|
| Vite 8 | Rolldown + Oxc | build.rolldownOptions, worker.rolldownOptions, optimizeDeps.rolldownOptions; the old rollupOptions/esbuild* keys still work with a deprecation warning |
| Vite 5-7 | esbuild (dev) + Rollup (build) | build.rollupOptions, optimizeDeps.esbuildOptions, esbuild |
- 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 strayvite.config.ts. - 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..localfiles must be gitignored. - Only variables with
envPrefix(defaultVITE_) are exposed to client code asimport.meta.env.VITE_X. They are inlined into the bundle as plain text: anything withVITE_is public. Never prefix API secrets. - Built-ins:
import.meta.env.MODE,DEV,PROD,SSR,BASE_URL. --mode stagingpicks.env.staging;NODE_ENVand 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.jsonat startup or have the server inject awindow.__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 need | Do |
|---|---|
| Import of a CJS dependency breaks only in dev | Add it to optimizeDeps.include |
| Linked monorepo package not updating in dev | Ensure it is not pre-bundled: optimizeDeps.exclude: ['@acme/ui'], and it ships ESM |
| Dev server in Docker or WSL, HMR dead | server.host: true, server.watch.usePolling: true (CPU cost), set server.hmr.clientPort if behind a proxy |
| Accessing dev server via a custom hostname | Add it to server.allowedHosts (blocked by default since the 2025 security fix) |
| One vendor chunk too large | Split by route with dynamic import() first; manual chunking second |
Need process.env.X from a legacy lib | define: { 'process.env.NODE_ENV': JSON.stringify(mode) }; do not polyfill all of process |
Node built-ins (Buffer, crypto) in browser code | Replace the dependency; polyfill plugins are a last resort |
| Serving under a sub-path | base: '/app/', and use import.meta.env.BASE_URL for runtime URLs |
| Assets that must keep their filename | public/, referenced by absolute path, never imported |
Plugins
- Order matters;
enforce: 'pre' | 'post'andapply: '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/clientandvite 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.externalfor heavy Node-only deps.- In dev, use
createServer({ server: { middlewareMode: true }, appType: 'custom' })andvite.ssrLoadModule(); in production, never import Vite. - Code that touches
windowmust be guarded byimport.meta.env.SSRor 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.tsruns in Node:import.meta.envis not populated there. UseloadEnv.vite previewservesdist/for a local smoke test; it is not a production server.index.htmlis the entry point and lives at the project root (orroot), not inpublic/.- Absolute imports like
/src/xwork in dev but resolve againstbasein build; use aliases. - Changing
optimizeDepssometimes needsnpx vite --forceto drop the stale cache innode_modules/.vite. - A
vitest.config.tsthat does notmergeConfigwith 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 --noEmitpasses 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-runlists onlydist, 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.yaml
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.