Skip to content

Vite integration

deno-inertia handles two distinct modes: development (Vite dev server + HMR) and production (compiled assets from the Vite manifest).

ts
import {
  viteDevScripts,
  viteProdAssets,
  readViteManifest,
  serveStaticAsset,
} from "deno-inertia"

Dev vs prod architecture

Dev  : Browser ←→ Deno (:3000)
                   Vite (:5173) ← HMR, transforms, hot reload

Prod : Browser ←→ Deno (:3000)
                   dist/ ← compiled, hashed, immutable assets

Development mode

viteDevScripts(config)

Generates <script> tags for the Vite client (HMR) and the frontend entry point.

ts
interface ViteDevConfig {
  url?:   string   // Vite server URL, default: "http://localhost:5173"
  entry:  string   // Entry path — e.g. "/src/main.ts"
  react?: boolean  // Inject React Refresh preamble (@react-refresh)
}
ts
// Vue
viteDevScripts({ entry: "/src/main.ts" })
// generates:
// <script type="module" src="http://localhost:5173/@vite/client"></script>
// <script type="module" src="http://localhost:5173/src/main.ts"></script>
ts
// React — react: true required
viteDevScripts({ entry: "/src/main.tsx", react: true })
// also generates:
// <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>

Why react: true?@vitejs/plugin-react requires a "preamble" script to install React Fast Refresh. When Vite serves the HTML itself (with index.html), it injects it automatically. Here Deno serves the HTML — it must be injected manually. Without this flag: "can't detect preamble. Something is wrong."

vite.config.ts — CORS required

The browser loads assets from Vite (:5173) but the page comes from Deno (:3000). CORS must be enabled in Vite:

ts
export default defineConfig({
  server: { cors: true },
  // ...
})

Production mode

readViteManifest(path)

Reads the manifest.json file generated by vite build. Call once at server startup.

ts
const manifest = await readViteManifest("dist/.vite/manifest.json")

viteProdAssets(entry, manifest, base?)

Generates <link rel="stylesheet">, <link rel="modulepreload"> and <script type="module"> tags from the Vite manifest.

ts
const assets = viteProdAssets("src/main.ts", manifest)
// → <link rel="modulepreload" href="/assets/vendor-abc123.js">
//   <link rel="stylesheet"    href="/assets/style-def456.css">
//   <script type="module"     src="/assets/main-ghi789.js"></script>
  • Recursively resolves CSS from imported chunks (code splitting)
  • Generates modulepreload for imported JS chunks
  • base — asset URL prefix, default /assets/

Loose entry fallback — If the exact entry key is not found in the manifest, viteProdAssets attempts to resolve it in order:

  1. Suffix match (e.g. "main.ts" matches "src/app/main.ts")
  2. First chunk with isEntry: true
  3. Throws if nothing matches

A console warning is logged when an approximate match is used (not an error, for better DX).

serveStaticAsset(request, distDir, base?)

Serves static files from the dist/ directory with Cache-Control: public, max-age=31536000, immutable.

ts
// In the main handler
async function handler(request: Request): Promise<Response> {
  const { pathname } = new URL(request.url)

  if (pathname.startsWith("/assets/")) {
    const res = await serveStaticAsset(request, "dist")
    return res ?? new Response("Not Found", { status: 404 })
  }

  return router.handler(request)
}

Returns null if the URL does not start with base.


Usage in InertiaConfig

The entry shorthand in createInertia() handles dev/prod detection automatically — no boilerplate needed. See configuration.md.

ts
const inertia = createInertia({
  entry: "src/main.ts",   // auto dev/prod via PROD_MODE
  // entry: "src/main.tsx", react: true,  // React
  template: (page, assets) => `...`,
})

Explicit mode

ts
const IS_PROD = Deno.env.get("PROD_MODE") === "1"
const manifest = IS_PROD ? await readViteManifest("dist/.vite/manifest.json") : null

const inertia = createInertia({
  ...(IS_PROD && manifest
    ? { prod: { manifest, entry: "src/main.ts" } }
    : { vite: { entry: "/src/main.ts" } }),      // Vue
    // : { vite: { entry: "/src/main.tsx", react: true } }),  // React
  // ...
})

Optional plugin: backend-reload

examples/vite_plugin_backend_reload.ts is a Vite plugin that watches backend files (server.ts, lib) and sends a full-reload to the browser when they change. The Deno server already restarts thanks to --watch, this plugin reloads the browser afterwards.

ts
// vite.config.ts
// import { backendReloadPlugin } from "./vite_plugin_backend_reload.ts"

export default defineConfig({
  plugins: [
    vue(),
    // backendReloadPlugin(), // uncomment to enable
  ],
})

Disabled by default — the double-reload can be disruptive when working only on the frontend.