---
title: "Vite Advanced"
description: "Advanced Vite 8 patterns including Rolldown-powered builds, advancedChunks, Environment API, plugin development, SSR configuration, library mode, and build optimization. Use when customizing build pipelines, creating plugins, or configuring multi-environment builds."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/vite-advanced"
---

# Vite Advanced

Advanced Vite 8 patterns including Rolldown-powered builds, advancedChunks, Environment API, plugin development, SSR configuration, library mode, and build optimization. Use when customizing build pipelines, creating plugins, or configuring multi-environment builds.

<span className="badge badge-gray">Reference</span> <span className="badge badge-yellow">medium</span>

> **Not directly invocable** — no slash command and no model auto-selection. An agent loads it explicitly via `Read()`.

<ContextualSkillSidebar slug="vite-advanced" />

> **Vite Advanced** Advanced Vite 8 patterns including Rolldown-powered builds, advancedChunks, Environment API, plugin development, SSR configuration, library mode, and build optimization. Use when customizing build pipelines, creating plugins, or configuring multi-environment builds.


# Vite Advanced Patterns

A wrapper around **Vite 8** (Rolldown-powered), not a copy of its documentation.
Vite's own docs are the source of truth for config keys, API surface, and migration
mechanics. This skill carries the house delta plus four enforceable rules.

Vite 8 replaces the esbuild+Rollup pipeline with **Rolldown** (Rust-based unified
bundler) and is the default for new projects. `advancedChunks` supersedes
`manualChunks`; `build.rollupOptions` becomes `build.rolldownOptions`.

## Upstream coverage (do not restate)

Fetch these from Vite rather than expecting them here. Rows marked with a rule keep a
narrower, enforceable house subset in that rule file: read the rule for the house
position, fetch the doc for the full API.

| Topic | Fetch from |
|-------|------------|
| Vite 7 to 8 migration: `rolldownOptions`, `transformWithOxc`, `moduleType: 'js'`, removed hooks and output formats, browser target bumps | https://vite.dev/guide/migration |
| Rolldown adoption path, `rolldown-vite`, Oxc, `advancedChunks` group syntax (house subset stays in `rules/vite-advanced-chunks.md`) | https://vite.dev/guide/rolldown |
| Build options: `target`, `minify`, `sourcemap`, `cssCodeSplit`, `cssMinify`, `assetsInlineLimit`, `chunkSizeWarningLimit` | https://vite.dev/config/build-options |
| Dependency pre-bundling and `optimizeDeps` include/exclude/force | https://vite.dev/guide/dep-pre-bundling |
| SSR: client and server entry points, middleware-mode dev server, production server wiring, streaming | https://vite.dev/guide/ssr |
| Environment API config and per-environment build output (house subset stays in `rules/vite-environments.md`) | https://vite.dev/guide/api-environment |
| Environment API for plugins: `this.environment`, `perEnvironmentPlugin`, `applyToEnvironment` | https://vite.dev/guide/api-environment-plugins |
| Environment API for frameworks: `createBuilder`, `buildApp`, `ModuleRunner` | https://vite.dev/guide/api-environment-frameworks |
| Plugin hook reference, virtual modules, `enforce`/`apply`, `handleHotUpdate` (house subset stays in `rules/vite-plugin-hooks.md`) | https://vite.dev/guide/api-plugin |
| Env variables and modes, `.env` files, the `VITE_` prefix | https://vite.dev/guide/env-and-mode |
| Deploying a static site and verifying with `vite preview` | https://vite.dev/guide/static-deploy |

Library mode is not in the table: `references/library-mode.md` and
`rules/vite-lib-config.md` still carry it in full.

## House delta

Read `$\{CLAUDE_PLUGIN_ROOT\}/skills/vite-advanced/references/ork-delta.md` for the rules Vite's docs do not
state: the Vite 8 adoption path for existing production apps, the `VITE_` secrets
rule, the production sourcemap setting, the house build budget, the `vite preview`
gate, chunk-name stability across the `advancedChunks` migration, the visualizer
diff requirement, and the `node_modules/.vite` cache-clearing step.

## House config

Not a Vite tutorial: these are the settings the delta above makes non-negotiable, in the
shape we actually ship them.

```ts
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    // 'hidden' emits maps for the error reporter without publishing source.
    // Never true (publishes source) and never 'inline' (inflates every JS file).
    sourcemap: 'hidden',
    rollupOptions: {
      output: {
        // Carry existing chunk NAMES across the manualChunks to advancedChunks
        // migration. A renamed group changes its filename and invalidates that
        // chunk for every returning visitor, even with identical contents.
        advancedChunks: {
          groups: [
            { name: 'vendor', test: /node_modules/, priority: 10 },
            { name: 'app', priority: 0 },
          ],
        },
      },
    },
  },
});
```

```bash
# Prove the build. `vite build` exiting 0 means the bundler finished, not that the
# app runs: base-path mistakes and dev-only env vars survive a green build.
vite build && vite preview      # then walk the routes, console must be clean

# Diff the chunk graph before and after any chunk-config change.
npx vite-bundle-visualizer

# Stale pre-bundle cache mimics a broken plugin. Clear it BEFORE editing plugin code.
rm -rf node_modules/.vite && vite --force
```

> `VITE_`-prefixed variables are inlined into the client bundle at build time. They are
> public bundle content, never secrets. Audit `.env.production` before deploy.

## Rules

| Rule | Covers |
|------|--------|
| `rules/vite-advanced-chunks.md` | Chunk splitting: `advancedChunks` groups, priority, `maxSize`, `minShareCount` |
| `rules/vite-environments.md` | `environments` config for client, SSR, and edge; per-environment `outDir` |
| `rules/vite-lib-config.md` | Library mode externals, dual ESM/CJS output, `exports` map, type declarations |
| `rules/vite-plugin-hooks.md` | Hook order, virtual modules via `resolveId` + `load`, `enforce` and `apply` |

## Key Decisions

| Decision | Recommendation |
|----------|----------------|
| New projects | **Vite 8** (default) |
| Existing production apps | Stage the upgrade through `rolldown-vite` before committing |
| Multi-env builds | Environment API (`environments` config) |
| Plugin scope | Use `this.environment` for env-aware plugins |
| SSR | Middleware mode for dev, separate builds for prod |
| Chunks | `advancedChunks` for Vite 8, `manualChunks` for Vite 7 compat |

## Related Skills

- `ork:react-server-components-framework` - SSR integration
- `ork:storybook-testing` - Component testing with Vitest
- `ork:performance` - Core Web Vitals targets behind the build budget

## References

Load on demand with `Read("$\{CLAUDE_PLUGIN_ROOT\}/skills/vite-advanced/references/&lt;file&gt;")`:

| File | Content |
|------|---------|
| `ork-delta.md` | House rules that Vite's docs do not state |
| `library-mode.md` | Building publishable npm packages |


---

## Rules (4)

### Split Vite chunks for granular caching and faster initial loads instead of single-bundle shipping — HIGH


## Vite: Chunk Optimization

Use `advancedChunks` (Vite 8+) or `manualChunks` (Vite 7) to split vendor and application code into separate, cacheable chunks. Assign priorities to resolve conflicts and use `maxSize` to prevent oversized bundles.

**Incorrect:**
```typescript
// No chunk config — everything in a single monolithic bundle
export default defineConfig({
  build: { rolldownOptions: {} },  // No advancedChunks or manualChunks
})
```

**Correct (Vite 8+ — advancedChunks):**
```typescript
export default defineConfig({
  build: {
    rolldownOptions: {
      output: {
        advancedChunks: {
          groups: [
            {
              name: 'react-vendor',
              test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
              priority: 30,
              minSize: 20000,
              maxSize: 200000,
            },
            {
              name: 'router',
              test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/,
              priority: 25,
            },
            {
              name: 'vendor',
              test: /[\\/]node_modules[\\/]/,
              priority: 5,
              maxSize: 500000,  // Auto-splits into vendor, vendor-1, etc.
            },
          ],
        },
      },
    },
  },
})
```

**Correct (Vite 7 — manualChunks):**
```typescript
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'router': ['react-router-dom'],
        },
      },
    },
  },
})
```

**Key rules:**
- Separate framework deps into a dedicated vendor chunk with the highest priority so it caches independently from app code.
- Add a catch-all `vendor` group at lowest priority with `maxSize` to prevent oversized bundles.
- Use `minShareCount` for shared UI libraries — only extract when imported by 2+ routes.
- When migrating Vite 7 to 8, convert package arrays to regex and make implicit ordering explicit via `priority`.
- `manualChunks` is deprecated in Vite 8 — prefer `advancedChunks` for new projects.

Reference: full `advancedChunks` option list at https://vite.dev/guide/rolldown; house
chunk-naming and verification rules in `references/ork-delta.md`.


### Configure Vite environment API to separate client and SSR build targets correctly — MEDIUM


## Vite: Environment API

Vite 6+ treats environments (client, SSR, edge) as first-class concepts, each with its own module graph, config, plugin pipeline, and build output. Use `environments` config instead of mixing targets in top-level config.

**Incorrect:**
```typescript
// Flat config — SSR and client share the same target and externals
export default defineConfig({
  build: {
    outDir: 'dist',
    target: 'node20',                            // Wrong for client!
    rolldownOptions: { external: ['cloudflare:workers'] },  // Wrong for client!
  },
  ssr: { noExternal: ['some-package'] },  // Legacy SSR config
})
```

**Correct:**
```typescript
export default defineConfig({
  build: { sourcemap: false },  // Shared config

  environments: {
    client: {
      build: { outDir: 'dist/client', manifest: true },
    },
    ssr: {
      build: {
        outDir: 'dist/server',
        target: 'node20',
        rolldownOptions: { output: { format: 'esm' } },
      },
    },
    edge: {
      resolve: { noExternal: true, conditions: ['edge', 'worker'] },
      build: {
        outDir: 'dist/edge',
        rolldownOptions: { external: ['cloudflare:workers'] },
      },
    },
  },
})
```

**Correct — environment-aware plugins:**
```typescript
export function envAwarePlugin(): Plugin {
  return {
    name: 'env-aware',
    transform(code, id) {
      const env = this.environment  // Available in Vite 6+
      if (env.name === 'ssr') return transformForSSR(code)
      if (env.name === 'edge') return transformForEdge(code)
      return transformForClient(code)
    },
  }
}
```

**Key rules:**
- Put only shared settings at the top level; environment-specific settings go under `environments.client`, `environments.ssr`, etc.
- Each environment gets its own module graph and build output — never share `outDir` between environments.
- Use `this.environment` in hooks to branch per environment; use `perEnvironmentPlugin()` to skip environments entirely.
- For edge runtimes, set `resolve.noExternal: true` and `resolve.conditions` for edge-specific package exports.
- Vite 7 requires Node.js 20.19+ or 22.12+ for `require(esm)` support.

Reference: https://vite.dev/guide/api-environment for the config surface and
https://vite.dev/guide/api-environment-plugins for `this.environment` and
`perEnvironmentPlugin`.


### Configure Vite library mode with correct externals, exports, and type declarations — HIGH


## Vite: Library Mode

Configure `build.lib` with proper entry points, externalize peer dependencies, and provide dual ESM/CJS output with TypeScript declarations.

**Incorrect:**
```typescript
// Bundles React into the library — consumers get duplicate React
export default defineConfig({
  build: {
    lib: { entry: resolve(__dirname, 'src/index.ts'), formats: ['es'] },
    // Missing rolldownOptions.external — peer deps are bundled
  },
})
```

**Correct:**
```typescript
import { defineConfig } from 'vite'
import { resolve } from 'path'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [dts({ include: ['src'], rollupTypes: true })],
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib',
      fileName: (format) => `my-lib.${format}.js`,
    },
    rolldownOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: { react: 'React', 'react-dom': 'ReactDOM' },
      },
    },
  },
})
```

```json
{
  "name": "my-lib",
  "type": "module",
  "main": "./dist/my-lib.umd.js",
  "module": "./dist/my-lib.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-lib.es.js",
      "require": "./dist/my-lib.umd.js",
      "types": "./dist/index.d.ts"
    },
    "./styles.css": "./dist/style.css"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "sideEffects": ["**/*.css"]
}
```

**Key rules:**
- Always externalize peer dependencies via `rolldownOptions.external` — never bundle them.
- Provide dual formats: ESM (`module`) for bundlers and UMD/CJS (`main`) for legacy consumers; use `exports` map.
- Generate TypeScript declarations with `vite-plugin-dts`; set `"types"` in top-level and each `exports` entry.
- Mark CSS in `"sideEffects"` so bundlers preserve styles during tree-shaking.
- For multi-entry libraries, use an object `entry` and match keys to `exports` subpaths.

Reference: `references/library-mode.md`


### Use correct Vite plugin hooks with enforce and apply modifiers to avoid silent failures — MEDIUM


## Vite: Plugin Hooks

Vite plugins follow a strict hook execution order inherited from Rollup. Choose the correct hook for each task and use `enforce`/`apply` to control when a plugin runs.

**Hook execution order:**
```
1. config          — Modify config before resolution
2. configResolved  — Access final config (read-only)
3. configureServer — Dev server setup (dev only)
4. buildStart      — Build begins
5. resolveId       — Resolve import paths to module IDs
6. load            — Provide module content for a resolved ID
7. transform       — Transform loaded module source code
8. buildEnd / closeBundle — Cleanup
```

**Incorrect:**
```typescript
// Wrong: transform can't create modules — virtual modules need resolveId + load
export function brokenVirtualPlugin(): Plugin {
  return {
    name: 'broken-virtual',
    transform(code, id) {
      if (id === 'virtual:my-data') {
        return `export default ${JSON.stringify({ key: 'value' })}`
      }
    },
  }
}
```

**Correct:**
```typescript
const VIRTUAL_ID = 'virtual:my-data'
const RESOLVED_ID = '\0' + VIRTUAL_ID

export function virtualDataPlugin(data: Record<string, unknown>): Plugin {
  return {
    name: 'virtual-data',
    resolveId(id) {
      if (id === VIRTUAL_ID) return RESOLVED_ID
    },
    load(id) {
      if (id === RESOLVED_ID) return `export default ${JSON.stringify(data)}`
    },
  }
}
```

**Correct — enforce and apply modifiers:**
```typescript
export function preProcessPlugin(): Plugin {
  return {
    name: 'pre-process',
    enforce: 'pre',    // Run BEFORE core Vite plugins
    apply: 'build',    // Only during vite build (not dev)
    transform(code, id) {
      if (!id.endsWith('.special.ts')) return null
      return { code: code.replace(/PLACEHOLDER/g, 'REPLACED'), map: null }
    },
  }
}
```

**Key rules:**
- Use `resolveId` + `load` for virtual modules; `transform` only modifies already-loaded source.
- Prefix resolved virtual IDs with `\0` to exclude them from other plugins and filesystem resolution.
- Set `enforce: 'pre'` to run before core plugins, `enforce: 'post'` to run after.
- Set `apply: 'build'` or `apply: 'serve'` to restrict a plugin to one mode.
- Access `this.environment` in hooks (Vite 6+) for environment-specific transforms.

Reference: full hook reference at https://vite.dev/guide/api-plugin.



---

## References (2)

### Library Mode

# Vite Library Mode

Building publishable npm packages.

## Basic Library Config

```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    react(),
    dts({ include: ['src'] }), // Generate .d.ts files
  ],

  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      name: 'MyLib', // Global variable name for UMD
      fileName: (format) => `my-lib.${format}.js`,
    },
    rolldownOptions: {
      // Externalize dependencies that shouldn't be bundled
      external: ['react', 'react-dom'],
      output: {
        // Global variables for UMD build
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM',
        },
      },
    },
  },
})
```

## Package.json Setup

```json
{
  "name": "my-lib",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/my-lib.umd.js",
  "module": "./dist/my-lib.es.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/my-lib.es.js",
      "require": "./dist/my-lib.umd.js",
      "types": "./dist/index.d.ts"
    },
    "./styles.css": "./dist/style.css"
  },
  "files": [
    "dist"
  ],
  "sideEffects": [
    "**/*.css"
  ],
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0",
    "react-dom": "^18.0.0 || ^19.0.0"
  },
  "devDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "vite": "^7.0.0",
    "vite-plugin-dts": "^4.0.0"
  },
  "scripts": {
    "build": "vite build",
    "dev": "vite"
  }
}
```

## Multiple Entry Points

```typescript
// vite.config.ts
export default defineConfig({
  build: {
    lib: {
      entry: {
        index: resolve(__dirname, 'src/index.ts'),
        utils: resolve(__dirname, 'src/utils/index.ts'),
        hooks: resolve(__dirname, 'src/hooks/index.ts'),
      },
      formats: ['es', 'cjs'],
    },
    rolldownOptions: {
      external: ['react', 'react-dom'],
    },
  },
})
```

With matching exports:

```json
{
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs",
      "types": "./dist/utils.d.ts"
    },
    "./hooks": {
      "import": "./dist/hooks.js",
      "require": "./dist/hooks.cjs",
      "types": "./dist/hooks.d.ts"
    }
  }
}
```

## CSS Handling

```typescript
// vite.config.ts
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
    },
    cssCodeSplit: false, // Bundle all CSS into one file
    rolldownOptions: {
      external: ['react', 'react-dom'],
    },
  },
})
```

For CSS modules with TypeScript:

```typescript
// vite.config.ts
export default defineConfig({
  css: {
    modules: {
      localsConvention: 'camelCase',
    },
  },
})
```

## Preserving File Structure

```typescript
// vite.config.ts
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/index.ts'),
      formats: ['es'],
    },
    rolldownOptions: {
      external: ['react', 'react-dom'],
      output: {
        preserveModules: true, // Keep file structure
        preserveModulesRoot: 'src',
        entryFileNames: '[name].js',
      },
    },
  },
})
```

## TypeScript Declarations

Install and configure vite-plugin-dts:

```bash
npm install -D vite-plugin-dts
```

```typescript
import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    dts({
      include: ['src'],
      exclude: ['src/**/*.test.ts', 'src/**/*.stories.tsx'],
      rollupTypes: true, // Bundle .d.ts files
    }),
  ],
})
```

## Development Testing

```typescript
// vite.config.ts
export default defineConfig(({ command }) => ({
  plugins: [react()],

  // Only apply library config for build
  ...(command === 'build' && {
    build: {
      lib: {
        entry: resolve(__dirname, 'src/index.ts'),
      },
    },
  }),
}))
```

## Pre-publish Checklist

```bash
# 1. Build
npm run build

# 2. Check output
ls -la dist/

# 3. Verify types
cat dist/index.d.ts

# 4. Test locally
cd ../test-project
npm link ../my-lib

# 5. Publish
npm publish
```


### Ork Delta

# Vite: the OrchestKit delta

Vite's own documentation is the source of truth for API surface, config keys, and
migration mechanics. This file holds only the house rules that vendor docs do not
state, plus the ones we hold harder than upstream does.

Vendor topics and where to fetch them live in the "Upstream coverage" table in
`SKILL.md`. The enforceable house subset for chunks, environments, library mode,
and plugin hooks lives in `rules/*.md` and is not repeated here.

## Default new Vite projects to 8; route existing production apps through rolldown-vite first

Vite 8's Rolldown pipeline changes bundler internals, not just config names, so a
large existing app can pass `vite build` and still ship different chunk graphs and
different tree-shaking outcomes. `rolldown-vite` is a one-line import swap that runs
the new bundler under the old package boundary, which makes it the cheap way to find
plugin incompatibilities before committing the upgrade. Greenfield work skips the
staging step and goes straight to `vite@8`.

Why: house decision, distilled from the retired references/vite8-rolldown.md; no traced incident.
Upstream: https://vite.dev/guide/rolldown

## Treat every VITE_-prefixed variable as public bundle content

Vite inlines `VITE_*` variables into the client bundle at build time. Anything put
there is readable by any visitor with devtools, so it is not a secret, not a
"private" API key, and not a soft-launch flag. Secrets belong on the server side of
whatever renders the app. Check `.env.production` against this rule before deploy,
not after.

Why: house security posture, distilled from the retired checklists/production-build.md; no traced incident.
Upstream: https://vite.dev/guide/env-and-mode

## Ship production sourcemaps as false or 'hidden', never true or inline

`sourcemap: true` publishes readable source next to the bundle; `'inline'` also
inflates every JS file. `'hidden'` emits the maps for an error reporter to upload
without referencing them from the shipped bundle, which is the only setting that
gives usable stack traces without publishing the source.

Why: house decision, distilled from the retired checklists/production-build.md; no traced incident.
Upstream: https://vite.dev/config/build-options

## Hold the house production build budget before deploying

These are the numbers a build is measured against, not Vite defaults:

| Metric | Budget |
|--------|--------|
| Initial JS | &lt; 200 kb gzipped |
| Main chunk | &lt; 150 kb |
| Vendor chunk | &lt; 100 kb |
| CSS | &lt; 50 kb |
| LCP | &lt; 2.5 s |
| TTI | &lt; 3.5 s |

A build over budget is a blocked deploy, not a warning. Vite's own
`chunkSizeWarningLimit` only prints a message and exits 0, so it cannot be the gate.

Why: house budget, distilled from the retired checklists/production-build.md; no traced incident.
Upstream: https://vite.dev/config/build-options

## Prove a build with vite preview, not with a green vite build

`vite build` exiting 0 says the bundler finished, not that the app runs. Base-path
mistakes, missing `public/` assets, and env vars that were only defined in dev all
survive a successful build and fail on first load. Serve the built output with
`vite preview`, walk the routes, and confirm the console is clean before calling a
build shippable.

Why: house verification gate, distilled from the retired checklists/production-build.md; no traced incident.
Upstream: https://vite.dev/guide/static-deploy

## Keep chunk names stable when migrating manualChunks to advancedChunks

Renaming a chunk group changes its output filename, which invalidates that chunk for
every returning visitor even when its contents did not change. Carry the existing
`name` values across the migration and make the old implicit ordering explicit with
`priority` instead of reshuffling groups.

Why: house decision, distilled from the retired references/chunk-optimization.md; no traced incident.
Upstream: https://vite.dev/guide/rolldown

## Diff bundle-visualizer output before and after any chunk-config change

Chunk config is guesswork until the graph is inspected. Capture a visualizer run
before the change and one after, then compare chunk count, per-chunk size, and
duplicate packages. A change that only moves bytes between chunks without shrinking
the initial payload is not an improvement.

Run it with `npx vite-bundle-visualizer`.

Why: house verification gate, distilled from the retired references/chunk-optimization.md; no traced incident.
Upstream: https://github.com/btd/rollup-plugin-visualizer (the tool itself; Vite's build guide does not document it)

## Clear node_modules/.vite before blaming a plugin for an unresolved import

Vite caches pre-bundled dependencies under `node_modules/.vite`. After a dependency
swap, a lockfile change, or a worktree switch, that cache can serve a stale module
graph and produce "cannot find module" for modules that plainly exist. Delete the
cache directory or rerun with `--force` and reproduce before editing plugin code.

Why: house debugging order, distilled from the retired checklists/production-build.md; no traced incident.
Upstream: https://vite.dev/guide/dep-pre-bundling
