import tailwindcss from "@tailwindcss/vite"; import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { defineConfig, type Plugin } from "vite"; import solid from "vite-plugin-solid"; const outputDirectory = process.env.WASMELD_WEB_OUT_DIR ?? "dist"; export default defineConfig({ server: { port: 3000, }, plugins: [solid(), tailwindcss(), pwaWorker()], build: { outDir: outputDirectory, emptyOutDir: true, rollupOptions: { // Host and Page remain separate HTML documents. Dynamic page imports // then create one lazy chunk per primary management view. input: { host: resolve(import.meta.dirname, "index.html"), page: resolve(import.meta.dirname, "page.html"), }, }, }, }); function pwaWorker(): Plugin { return { name: "wasmeld-pwa-worker", apply: "build", generateBundle(_, bundle) { // Derive the cache name from emitted filenames and unhashed public // assets. A changed build activates a new cache without another PWA // dependency or a generated source file in the repository. const publicFiles = ["icon.svg", "manifest.webmanifest", "og.png"]; const generatedFiles = Object.keys(bundle).filter((file) => /\.(?:html|js|css|svg|png|webmanifest)$/.test(file), ); const files = [...new Set([...generatedFiles, ...publicFiles])].sort(); const digest = createHash("sha256"); for (const file of files) digest.update(file); for (const file of publicFiles) { digest.update(readFileSync(resolve(import.meta.dirname, "public", file))); } const cacheName = `wasmeld-${digest.digest("hex").slice(0, 16)}`; const urls = files.map((file) => `/${file}`); this.emitFile({ type: "asset", fileName: "sw.js", source: serviceWorkerSource(cacheName, urls), }); }, }; } function serviceWorkerSource(cacheName: string, urls: string[]): string { // Runtime state must never be satisfied from an old cache. Only documents // and build assets participate in offline behavior. return `const CACHE = ${JSON.stringify(cacheName)}; const PRECACHE = ${JSON.stringify(urls)}; self.addEventListener("install", (event) => { event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(PRECACHE)).then(() => self.skipWaiting())); }); self.addEventListener("activate", (event) => { event.waitUntil(caches.keys().then((keys) => Promise.all(keys.filter((key) => key.startsWith("wasmeld-") && key !== CACHE).map((key) => caches.delete(key)))).then(() => self.clients.claim())); }); self.addEventListener("fetch", (event) => { if (event.request.method !== "GET") return; const url = new URL(event.request.url); if (url.origin !== self.location.origin || url.pathname === "/healthz" || url.pathname.startsWith("/api/")) return; if (event.request.mode === "navigate") { event.respondWith(fetch(event.request).then((response) => { const copy = response.clone(); caches.open(CACHE).then((cache) => cache.put(event.request, copy)); return response; }).catch(async () => { const cached = await caches.match(event.request); if (cached) return cached; return caches.match(url.pathname === "/page.html" ? "/page.html" : "/index.html"); })); return; } event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request))); }); `; }