Phase 0 scaffolding for the frontend: SvelteKit + adapter-static in SPA mode
(fallback index.html, ssr disabled in the root layout — no Node process in
production, Caddy serves build/ directly per docs/PLAN.md), a login page and
auth store backed by /api/v1/auth/{login,me,logout}, an installable-PWA shell
(hand-written manifest, iOS meta/safe-area handling, an install-onboarding
banner), and a Dockerfile whose only job is to produce a buildable /app/build
artifact for deploy/ to consume.
Service worker notes, since the wiring isn't obvious from the diff:
- injectManifest, not generateSW: the caching policy needs to say "never
cache /api/*", which generateSW's declarative config can't express as
precisely as hand-written Workbox routes can.
- Uses the base `vite-plugin-pwa` plugin, not `@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 the file — but that native build only permits importing
SvelteKit's own three virtual modules and hard-rejects `workbox-*` imports,
which our SW needs. The base plugin bundles src/service-worker.ts directly
instead, which works. SvelteKit's native service-worker convention is
explicitly disabled (`kit.files.serviceWorker` pointed at a path that
doesn't exist) so the two builds can't collide and silently clobber each
other's output — they do, if both are left enabled, and the failure mode is
silent (the SW builds fine, just precaches nothing).
- workbox-core/precaching/routing/strategies had to be added as direct
devDependencies even though workbox-build depends on them — pnpm doesn't
hoist transitive deps into the top-level node_modules, so the SW bundle
step couldn't resolve them otherwise.
- The SPA fallback index.html doesn't exist yet at service-worker-build time
(adapter-static writes it after all Vite plugins finish), so it can't be
glob-hashed into the precache manifest normally. It gets a synthetic
manifest entry instead, revisioned by a per-build-invocation timestamp
(see the swIndexRevision comment in vite.config.ts) so the cached shell
still invalidates correctly on every deploy.
Full rationale for each choice is inline as comments in vite.config.ts and
apps/web/README.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
95 lines
4.4 KiB
TypeScript
95 lines
4.4 KiB
TypeScript
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'
|
|
}
|
|
})
|
|
]
|
|
});
|