Kez/kez-chat/web/vite.config.ts
Jason Tudisco 6c0f5e2fd5 feat(kez-chat/web): make the SPA installable as a PWA
Now installable on iOS (Safari → Share → Add to Home Screen) and
Android/desktop Chrome (install prompt or Settings → Install app).
Launches in standalone mode with a dark theme color matching the
lock-icon palette.

Stack:
  • vite-plugin-pwa with workbox in generateSW mode, registerType
    'autoUpdate' — new SW activates on next page load, no upgrade prompt
    (chat needs to stay fresh).
  • @vite-pwa/assets-generator for icon variants from a single SVG.
    Source kez-icon.svg = dark squircle (#111827) + amber key glyph,
    drawn inside the 80% maskable safe zone.

Caching:
  • Precaches the SPA shell (~635 KB inc. the zstd WASM, well under
    the 5 MB per-file cap).
  • runtimeCaching 'NetworkOnly' for /v1/* — never cache authenticated
    chat data; every poll must hit the network.
  • navigateFallback to index.html so /messages, /claims, /dashboard
    survive a refresh while offline. The /v1/, /internal/, /.well-known/
    paths are explicitly denylisted from this fallback.

Meta tags (index.html):
  • <link rel="manifest"> + theme-color for Android Chrome.
  • apple-touch-icon-180x180 + apple-mobile-web-app-* meta for iOS,
    including status-bar-style=black-translucent so the dark header
    flows into the notch area in standalone.
  • viewport-fit=cover so safe-area-inset works on notched devices.

Generated artifacts committed under web/public/:
  kez-icon.svg, pwa-{64,192,512}.png, maskable-icon-512x512.png,
  apple-touch-icon-180x180.png, favicon.ico.

Verified live: /manifest.webmanifest serves application/manifest+json,
/sw.js serves text/javascript, all icons return 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:23:14 -06:00

78 lines
2.6 KiB
TypeScript

import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import tailwindcss from "@tailwindcss/vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
svelte(),
tailwindcss(),
VitePWA({
// Auto-update: a new SW activates on next page load. No "click to
// update" prompt — chat needs to stay fresh and we don't want users
// stuck on an old build.
registerType: "autoUpdate",
injectRegister: "auto",
manifest: {
name: "kez-chat",
short_name: "kez-chat",
description:
"End-to-end encrypted chat on top of KEZ — portable cross-app identity.",
start_url: "/",
scope: "/",
display: "standalone",
background_color: "#111827",
theme_color: "#111827",
categories: ["social", "communication"],
icons: [
{ src: "pwa-64x64.png", sizes: "64x64", type: "image/png" },
{ src: "pwa-192x192.png", sizes: "192x192", type: "image/png" },
{ src: "pwa-512x512.png", sizes: "512x512", type: "image/png" },
{
src: "maskable-icon-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
],
},
workbox: {
// Precache the SPA shell. Chat data is fetched live from /v1/*
// and we DON'T want it cached — see runtimeCaching below.
globPatterns: ["**/*.{js,css,html,svg,png,ico,wasm}"],
// zstd wasm is ~350 KB; raise the per-file cap.
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
// Same-origin navigation requests fall back to the SPA shell so
// /messages, /claims, etc. work after a refresh while offline.
navigateFallback: "index.html",
navigateFallbackDenylist: [/^\/v1\//, /^\/internal\//, /^\/\.well-known\//],
runtimeCaching: [
{
// Never cache API responses — they're authenticated + dynamic.
urlPattern: /\/v1\//,
handler: "NetworkOnly",
},
],
navigationPreload: true,
cleanupOutdatedCaches: true,
},
devOptions: {
enabled: false,
},
}),
],
server: {
// For dev: proxy API calls to the locally-running chat-server.
// The deployed SPA (served by the same chat-server) doesn't need this.
proxy: {
"/v1": "http://localhost:6969",
"/internal": "http://localhost:6969",
"/.well-known": "http://localhost:6969",
},
},
build: {
outDir: "dist",
emptyOutDir: true,
},
});