.claude/skills/vite/vite-impl/vite-impl-backend-integration/SKILL.md
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer vite-impl-backend-integrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Use this skill when:
.vite/manifest.json to resolve hashed asset paths| Concern | Development | Production |
|---------|------------|------------|
| Asset serving | Vite dev server (localhost:5173) | Static files from dist/ |
| HTML generation | Backend injects Vite client + entry scripts | Backend reads manifest.json, renders tags |
| HMR | Automatic via @vite/client | N/A |
| CSS | Injected by Vite dev server | Extracted as separate files |
| Entry resolution | Direct source path | Hashed filename from manifest |
NEVER omit import 'vite/modulepreload-polyfill' from your entry point -- without it, <link rel="modulepreload"> tags will not work in browsers that lack native support.
NEVER hardcode hashed filenames in production templates -- ALWAYS read .vite/manifest.json at runtime or build time. Hashed names change on every build.
NEVER serve the Vite dev server scripts in production -- ALWAYS use environment detection to switch between dev mode HTML and production manifest rendering.
NEVER omit the React preamble when using @vitejs/plugin-react with a backend -- HMR will silently fail without it.
ALWAYS set build.manifest: true in vite.config.ts when integrating with a backend framework.
ALWAYS set server.cors to allow your backend origin during development.
ALWAYS specify build.rollupOptions.input (Vite 6/7) or build.rolldownOptions.input (Vite 8+) to define your entry point explicitly.
import { defineConfig } from 'vite'
export default defineConfig({
server: {
cors: {
origin: 'http://my-backend.example.com',
},
},
build: {
manifest: true,
// Vite 6/7:
rollupOptions: {
input: '/path/to/main.js',
},
// Vite 8+:
// rolldownOptions: {
// input: '/path/to/main.js',
// },
},
})
ALWAYS add the modulepreload polyfill as the first import in your entry file:
// main.js
import 'vite/modulepreload-polyfill'
// Rest of your application code
import './styles/app.css'
import { createApp } from './app'
createApp()
In development, your backend template MUST include two scripts pointing to the Vite dev server:
<!-- Backend template: development mode -->
<html>
<head>
<!-- No CSS links needed -- Vite injects CSS via JS in dev -->
</head>
<body>
<div id="app"></div>
<!-- 1. Vite client for HMR -->
<script type="module" src="http://localhost:5173/@vite/client"></script>
<!-- 2. Your entry point -->
<script type="module" src="http://localhost:5173/main.js"></script>
</body>
</html>
When using @vitejs/plugin-react, ALWAYS inject the React Refresh preamble BEFORE the Vite client script:
<!-- React preamble: MUST come before @vite/client -->
<script type="module">
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<!-- Then Vite client -->
<script type="module" src="http://localhost:5173/@vite/client"></script>
<!-- Then entry -->
<script type="module" src="http://localhost:5173/main.js"></script>
After running vite build, the manifest file is located at:
dist/.vite/manifest.json
| Property | Type | Description |
|----------|------|-------------|
| src | string | Original source file path |
| file | string | Hashed output file path |
| css | string[] | CSS files associated with this chunk |
| assets | string[] | Non-JS/CSS assets imported by this chunk |
| isEntry | boolean | Whether this is an entry point |
| name | string | Short name of the chunk |
| isDynamicEntry | boolean | Whether this is a dynamic import entry |
| imports | string[] | Keys of statically imported chunks |
| dynamicImports | string[] | Keys of dynamically imported chunks |
For each entry point, ALWAYS render tags in this exact order:
<link rel="stylesheet"> for the entry chunk's own CSS files<link rel="stylesheet"> for all recursively imported chunks' CSS files<script type="module"> for the entry chunk's JS file<link rel="modulepreload"> for all recursively imported JS chunks<!-- 1. Entry CSS -->
<link rel="stylesheet" href="/dist/assets/main-5UjPuW-k.css" />
<!-- 2. Imported chunks' CSS (recursive) -->
<link rel="stylesheet" href="/dist/assets/shared-ChJ_j-JJ.css" />
<!-- 3. Entry JS -->
<script type="module" src="/dist/assets/main-BRBmoGS9.js"></script>
<!-- 4. Modulepreload for imported JS -->
<link rel="modulepreload" href="/dist/assets/shared-B7PI925R.js" />
Use this recursive algorithm to collect all imported chunks (prevents duplicates via seen Set):
import type { Manifest, ManifestChunk } from 'vite'
export default function importedChunks(
manifest: Manifest,
name: string,
): ManifestChunk[] {
const seen = new Set<string>()
function getImportedChunks(chunk: ManifestChunk): ManifestChunk[] {
const chunks: ManifestChunk[] = []
for (const file of chunk.imports ?? []) {
const importee = manifest[file]
if (seen.has(file)) continue
seen.add(file)
chunks.push(...getImportedChunks(importee))
chunks.push(importee)
}
return chunks
}
return getImportedChunks(manifest[name])
}
Is this a development environment?
├─ YES → Inject Vite dev server scripts into HTML
│ ├─ Using React? → Add React preamble FIRST
│ ├─ Add @vite/client script
│ └─ Add entry point script
└─ NO → Read .vite/manifest.json
├─ Look up entry chunk by source path
├─ Collect CSS from entry + recursive imports
├─ Render <link rel="stylesheet"> tags
├─ Render <script type="module"> for entry
└─ Render <link rel="modulepreload"> for imports
# Example: Python/Django-style pseudocode
def vite_tags(entry: str) -> str:
if settings.DEBUG:
return dev_tags(entry)
else:
return production_tags(entry)
def dev_tags(entry: str) -> str:
return f'''
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/{entry}"></script>
'''
def production_tags(entry: str) -> str:
manifest = load_manifest('dist/.vite/manifest.json')
chunk = manifest[entry]
imported = get_imported_chunks(manifest, entry)
tags = []
# 1. Entry CSS
for css in chunk.get('css', []):
tags.append(f'<link rel="stylesheet" href="/dist/{css}" />')
# 2. Imported CSS
for imp in imported:
for css in imp.get('css', []):
tags.append(f'<link rel="stylesheet" href="/dist/{css}" />')
# 3. Entry JS
tags.append(f'<script type="module" src="/dist/{chunk["file"]}"></script>')
# 4. Modulepreload
for imp in imported:
tags.append(f'<link rel="modulepreload" href="/dist/{imp["file"]}" />')
return '\n'.join(tags)
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.
tools
Use when configuring SSR environments, edge workers, custom environments, or migrating to the Vite 6+ environment model. Prevents using the Environment API on Vite 5 (where it does not exist) and misconfiguring per-environment vs shared plugin scoping. Covers per-environment build and dev settings, EnvironmentOptions interface, custom environment providers, shared vs per-environment plugins, configuration inheritance, and migration from Vite 5 implicit environments. Keywords: vite, environment api, vite 6, SSR, edge workers, per-environment config, providers.