import adapter from '@sveltejs/adapter-static'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; import { VitePWA } from 'vite-plugin-pwa'; // The adapter-static SPA fallback (build/index.html) is written by the adapter // *after* every Vite plugin's build has finished, so at service-worker-build // time there's no file for injectManifest to glob and hash. We instead give it // a synthetic precache entry (below) with a revision that's fresh per build // invocation, so the shell still gets correctly invalidated on every deploy. const swIndexRevision = String(Date.now()); export default defineConfig({ server: { proxy: { // Local dev only. In production Caddy does this same-origin proxy // (see deploy/Caddyfile) — the app only ever calls same-origin /api/v1. '/api': { target: 'http://localhost:8000', changeOrigin: true } } }, plugins: [ sveltekit({ compilerOptions: { // Force runes mode for the project, except for libraries. Can be removed in svelte 6. runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true }, // SvelteKit has its own built-in convention for src/service-worker.{js,ts}: // it auto-detects the file by this exact path and runs its own separate, // very restricted build for it (only $service-worker, $env/static/public and // $app/env/public may be imported — nothing from npm). vite-plugin-pwa (below) // bundles and injects our actual service worker instead; if both run, the // SvelteKit's native pass executes *after* vite-plugin-pwa's and silently // overwrites its output with an un-bundled, un-injected file (the // self.__WB_MANIFEST placeholder is left as literal text — the app "works" // but nothing is ever precached). Point SvelteKit's lookup at a path that // doesn't exist so only vite-plugin-pwa's build runs. files: { serviceWorker: 'src/service-worker-disabled' }, // Static SPA: no Node process in production. Caddy serves the build output // directly and falls back to index.html for client-side routes (see // docs/PLAN.md "Stack" and "The PWA decision"). Pair with `export const ssr = // false` in src/routes/+layout.ts — without that, SvelteKit still tries to // prerender/SSR routes and the static adapter fails the build. adapter: adapter({ fallback: 'index.html' }) }), VitePWA({ // injectManifest, not generateSW: we hand-write the caching policy (cache-first // navigations, network-only for /api/*) in src/service-worker.ts. generateSW's // declarative config can't express "never cache this origin path" as precisely // as we want here — see docs/PLAN.md's caching table. // // We use the base vite-plugin-pwa plugin rather than @vite-pwa/sveltekit's // SvelteKit-specific wrapper: that wrapper's injectManifest build expects // SvelteKit's own built-in src/service-worker.{js,ts} convention to have // already transpiled our file, which only resolves SvelteKit's own three // virtual modules and rejects anything else. The base plugin bundles // src/service-worker.ts itself directly, which is what we want here. strategies: 'injectManifest', srcDir: 'src', filename: 'service-worker.ts', // We own the manifest as a plain static file (static/manifest.webmanifest), // linked explicitly from app.html, instead of letting the plugin generate one. manifest: false, // We register the service worker ourselves via virtual:pwa-register/svelte // (see src/lib/components/UpdateToast.svelte) so we can show an in-app update // prompt instead of the plugin's default injected register script. injectRegister: false, injectManifest: { injectionPoint: 'self.__WB_MANIFEST', // Default globPatterns is just JS/CSS/HTML — widen it to also precache // our icons and manifest so an offline install has everything it needs. globPatterns: ['**/*.{js,css,svg,png,webmanifest}'], // See the swIndexRevision comment above: the real build/index.html // doesn't exist yet when this runs, so we can't glob-hash it. additionalManifestEntries: [{ url: 'index.html', revision: swIndexRevision }] }, devOptions: { // Keep the SW out of `pnpm dev` — dev already has instant HMR, and a dev-mode // SW is a common source of "why is my change not showing up" confusion. enabled: false, type: 'module' } }) ] });