---
name: vite-config-and-plugins
description: >-
  Vite config and plugin architecture: plugin ordering and enforce hooks,
  alias contracts with tsconfig, single-file environment branching, and
  scoping plugins to dev or build. Use when adding plugins or changing
  vite.config.ts.
argument-hint: "[plugin or config change]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Config & Plugin Architecture

Apply when adding a plugin or changing vite.config.ts. Before adding a plugin, check whether an existing one already covers the transform, and place the new one in the array where its input is guaranteed to be in the right shape.

## Rules

1. **Plugin order is execution order, not just a list** — Vite plugins run in the order declared (respecting `enforce: 'pre'|'post'`), and two plugins that transform the same file produce different output depending on which runs first. Treat the plugins array as a pipeline, not a bag.
2. **`resolve.alias` is a build-time contract, not documentation** — An alias that isn't mirrored in `tsconfig.json`'s `paths` compiles fine and breaks type-checking, or the reverse. The two must agree, or editors and the build silently disagree about what a path means.
3. **One config file, environment-branched inside it** — `defineConfig` receiving a function `(env) => ({...})` beats maintaining `vite.config.dev.ts` / `vite.config.prod.ts` siblings that drift from each other the first time someone updates only one.
4. **A plugin's job is one transform, not a grab bag** — A plugin that both transforms SVGs and injects env vars is two plugins wearing one name. Splitting them makes each independently testable and independently disable-able.
5. **Know which plugins run in dev vs build** — Some plugins only run for production builds via `apply: 'build'`. A bug that 'only happens in prod' is often a plugin that never ran in dev at all.

## Patterns

### Plugin order determining output

**Avoid** — legacy() runs first and tries to transpile syntax that hasn't been compiled from JSX yet — order matters, and this one is backwards.

```ts
export default defineConfig({
  plugins: [
    legacy({ targets: ["defaults"] }), // transpiles down...
    react(),                            // ...before JSX is even compiled
  ],
});
```

**Prefer** — Each plugin receives output it can actually operate on, because the pipeline runs in the order the array declares.

```ts
export default defineConfig({
  plugins: [
    react(),                            // compile JSX/TS first
    legacy({ targets: ["defaults"] }),  // then transpile the output
  ],
});
```

### Alias drifting from tsconfig

**Avoid** — The build and the type-checker/editor disagree about what `@` means. Red squiggles everywhere, build still passes.

```ts
// vite.config.ts
export default defineConfig({
  resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
});

// tsconfig.json has no matching "paths" entry — the build resolves
// "@/lib/utils" but the editor reports "Cannot find module".
```

**Prefer** — Both tools agree on the same alias, from the same source of truth stated deliberately in two places.

```ts
// vite.config.ts
export default defineConfig({
  resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
});

// tsconfig.json
// { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }
```

### Config duplicated per environment vs branched in one file

**Avoid** — Three near-identical files that only stay in sync if someone remembers to edit all three.

```ts
// vite.config.dev.ts, vite.config.staging.ts, vite.config.prod.ts
// Three files, one of which didn't get the last plugin update.
```

**Prefer** — One file, one source of truth, branching on `mode` instead of duplicating structure.

```ts
export default defineConfig(({ mode }) => ({
  plugins: [react(), mode === "production" && legacy()].filter(Boolean),
  define: {
    __API_URL__: JSON.stringify(mode === "production" ? PROD_API : DEV_API),
  },
}));
```

## Review checklist

- [ ] Plugin array order reflects the actual transform pipeline, with `enforce` used where order alone can't express it.
- [ ] `resolve.alias` and `tsconfig.json`'s `paths` are kept in agreement, not maintained independently.
- [ ] One `vite.config.ts` branches on `mode`/`command` instead of multiple near-duplicate config files.
- [ ] Each custom plugin does one transform and is named for what it does.
- [ ] `apply: 'build'` / `apply: 'serve'` is used deliberately when a plugin shouldn't run in both contexts.
- [ ] Config changes are verified in both `vite dev` and `vite build` before merging, not just one.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim