diff --git a/index.html b/index.html index 5d578b6..6e51dd4 100644 --- a/index.html +++ b/index.html @@ -188,11 +188,6 @@ - + diff --git a/js/main.js b/js/main.js index 79f70d3..a9185e2 100644 --- a/js/main.js +++ b/js/main.js @@ -135,3 +135,8 @@ function main() { } main(); + +// Register service worker only in production builds +if (import.meta.env.PROD && 'serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(() => {}); +} diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..28d2aca --- /dev/null +++ b/sw.js @@ -0,0 +1,39 @@ +/* sw.js — Matrix PWA service worker + * PRECACHE_URLS is replaced at build time by the generate-sw Vite plugin. + * Registration is gated to import.meta.env.PROD in main.js, so this template + * file is never evaluated by browsers during development. + */ +const CACHE_NAME = 'timmy-matrix-v1'; +const PRECACHE_URLS = __PRECACHE_URLS__; + +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME).then(cache => cache.addAll(PRECACHE_URLS)) + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys().then(keys => + Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener('fetch', event => { + if (event.request.method !== 'GET') return; + event.respondWith( + caches.match(event.request).then(cached => { + if (cached) return cached; + return fetch(event.request).then(response => { + if (!response || response.status !== 200 || response.type !== 'basic') { + return response; + } + caches.open(CACHE_NAME).then(cache => cache.put(event.request, response.clone())); + return response; + }); + }) + ); +}); diff --git a/vite.config.js b/vite.config.js index 165bcfb..4ff1b15 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,35 @@ import { defineConfig } from 'vite'; +import { readFileSync, writeFileSync } from 'fs'; + +/** Vite plugin: generates dist/sw.js with precache URLs from the build manifest. */ +function generateSW() { + return { + name: 'generate-sw', + apply: 'build', + closeBundle() { + const staticAssets = [ + '/', + '/manifest.json', + '/icons/icon-192.svg', + '/icons/icon-512.svg', + ]; + + try { + const manifest = JSON.parse(readFileSync('dist/.vite/manifest.json', 'utf-8')); + for (const entry of Object.values(manifest)) { + staticAssets.push('/' + entry.file); + if (entry.css) entry.css.forEach(f => staticAssets.push('/' + f)); + } + } catch { /* manifest may not exist in dev */ } + + const template = readFileSync('sw.js', 'utf-8'); + const out = template.replace('__PRECACHE_URLS__', JSON.stringify(staticAssets, null, 4)); + writeFileSync('dist/sw.js', out); + + console.log('[generate-sw] wrote dist/sw.js with', staticAssets.length, 'precache URLs'); + }, + }; +} export default defineConfig({ root: '.', @@ -6,6 +37,7 @@ export default defineConfig({ outDir: 'dist', assetsDir: 'assets', target: 'esnext', + manifest: true, rollupOptions: { output: { manualChunks: { @@ -14,6 +46,7 @@ export default defineConfig({ }, }, }, + plugins: [generateSW()], server: { host: true, },