import { execSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { defineConfig } from "vite"; import { svelte } from "@sveltejs/vite-plugin-svelte"; import tailwindcss from "@tailwindcss/vite"; import { VitePWA } from "vite-plugin-pwa"; // Build-time identity: short git SHA + ISO date. Surfaced in the footer // as "kez-chat " so users can eyeball which build they're on (handy // when verifying that a deploy actually landed instead of being cached). // // Resolution order, so it works both locally (with .git) and in the // Docker build on the remote (no .git copied): // 1. process.env.BUILD_SHA — set by deploy script // 2. ./BUILD_SHA file in this dir — set by deploy script // 3. `git rev-parse --short HEAD` — local dev // 4. "dev" — give up const BUILD_SHA = (() => { if (process.env.BUILD_SHA) return process.env.BUILD_SHA.trim(); if (existsSync("./BUILD_SHA")) { return readFileSync("./BUILD_SHA", "utf-8").trim(); } try { return execSync("git rev-parse --short HEAD").toString().trim(); } catch { return "dev"; } })(); const BUILD_TIME = new Date().toISOString(); export default defineConfig({ define: { __BUILD_SHA__: JSON.stringify(BUILD_SHA), __BUILD_TIME__: JSON.stringify(BUILD_TIME), }, 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: "#0b0c0e", theme_color: "#0b0c0e", 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: { // Activate new SW immediately + take control of existing pages // without waiting for them to close. Paired with the // controllerchange reload in main.ts, this means deploys land // on the first refresh instead of the second. skipWaiting: true, clientsClaim: true, // 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, }, });