import { fileURLToPath } from 'node:url';
import path from 'node:path';
import { createHash } from 'node:crypto';
import preact from '@preact/preset-vite';
import { defineConfig, type Plugin } from 'vite';
const rootDir = fileURLToPath(new URL('.', import.meta.url));
function buildServiceWorkerSource(precacheUrls: string[], version: string): string {
return `const CACHE_VERSION = ${JSON.stringify(`nodewarden-pwa-${version}`)};
const APP_SHELL_CACHE = \`\${CACHE_VERSION}-shell\`;
const RUNTIME_CACHE = 'nodewarden-pwa-runtime-v1';
const PRECACHE_URLS = ${JSON.stringify(precacheUrls, null, 2)};
const CRITICAL_SHELL_URLS = ['/', '/index.html'];
const STATIC_PATH_RE = /^\\/(?:assets\\/|payment-logos\\/|icon-|logo-|favicon|apple-touch-icon|nodewarden-|manifest\\.webmanifest$)/;
const NEVER_CACHE_PATH_RE = /^\\/(?:api|identity|setup|config|notifications|icons|\\.well-known|cdn-cgi)(?:\\/|$)/;
const OFFLINE_FALLBACK_HTML = '
NodeWarden
NodeWarden
Offline cache is not ready on this device. Open NodeWarden once while online, then try offline again.
';
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(APP_SHELL_CACHE)
.then(async (cache) => {
await cache.addAll(CRITICAL_SHELL_URLS);
const nonCriticalUrls = PRECACHE_URLS.filter((url) => !CRITICAL_SHELL_URLS.includes(url));
await Promise.allSettled(nonCriticalUrls.map((url) => cache.add(url)));
})
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(
keys
.filter((key) => key.startsWith('nodewarden-pwa-') && key.endsWith('-shell') && key !== APP_SHELL_CACHE)
.map((key) => caches.delete(key))
))
.then(() => self.clients.claim())
);
});
function isSameOriginHttpGet(request) {
if (request.method !== 'GET') return false;
const url = new URL(request.url);
return url.origin === self.location.origin;
}
function isCacheableResponse(response) {
return response && response.ok && (response.type === 'basic' || response.type === 'default');
}
async function refreshNavigationCache(request) {
const cache = await caches.open(APP_SHELL_CACHE);
try {
const response = await fetch(request);
if (isCacheableResponse(response)) {
await cache.put('/', response.clone());
await cache.put('/index.html', response.clone());
await warmStaticDependencies(response.clone());
}
return response;
} catch {
return null;
}
}
async function warmStaticDependencies(response) {
try {
const html = await response.text();
const runtimeCache = await caches.open(RUNTIME_CACHE);
const urls = Array.from(html.matchAll(/\\b(?:src|href)=["']([^"']+)["']/g))
.map((match) => {
try {
return new URL(match[1], self.location.origin);
} catch {
return null;
}
})
.filter((url) => url && url.origin === self.location.origin && STATIC_PATH_RE.test(url.pathname))
.map((url) => url.pathname + url.search);
await Promise.allSettled(Array.from(new Set(urls)).map((url) => runtimeCache.add(url)));
await trimRuntimeCache(runtimeCache, 120);
} catch {
// Dependency warming is best-effort; never slow or break navigation for it.
}
}
async function appShellNavigation(request) {
const cache = await caches.open(APP_SHELL_CACHE);
const url = new URL(request.url);
return (
(await cache.match(request, { ignoreSearch: true }))
|| (await cache.match(url.pathname, { ignoreSearch: true }))
|| (await cache.match('/'))
|| (await cache.match('/index.html'))
|| new Response(OFFLINE_FALLBACK_HTML, {
status: 200,
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
);
}
async function trimRuntimeCache(cache, maxEntries) {
const keys = await cache.keys();
if (keys.length <= maxEntries) return;
await Promise.all(keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key)));
}
async function cacheFirst(request) {
const shellCache = await caches.open(APP_SHELL_CACHE);
const cachedShell = await shellCache.match(request);
if (cachedShell) return cachedShell;
const runtimeCache = await caches.open(RUNTIME_CACHE);
const cachedRuntime = await runtimeCache.match(request);
if (cachedRuntime) return cachedRuntime;
const legacyRuntime = await matchLegacyRuntimeCache(request);
if (legacyRuntime) return legacyRuntime;
const response = await fetch(request);
if (isCacheableResponse(response)) {
void runtimeCache.put(request, response.clone()).then(() => trimRuntimeCache(runtimeCache, 120));
}
return response;
}
async function matchLegacyRuntimeCache(request) {
const keys = await caches.keys();
for (const key of keys) {
if (key === RUNTIME_CACHE || !key.startsWith('nodewarden-pwa-') || !key.endsWith('-runtime')) continue;
const cache = await caches.open(key);
const cached = await cache.match(request);
if (cached) return cached;
}
return null;
}
self.addEventListener('fetch', (event) => {
const request = event.request;
if (!isSameOriginHttpGet(request)) return;
const url = new URL(request.url);
if (NEVER_CACHE_PATH_RE.test(url.pathname)) return;
if (request.mode === 'navigate') {
event.respondWith(appShellNavigation(request));
if (navigator.onLine !== false) {
event.waitUntil(refreshNavigationCache(request));
}
return;
}
if (STATIC_PATH_RE.test(url.pathname) || request.destination === 'script' || request.destination === 'style' || request.destination === 'font' || request.destination === 'image' || request.destination === 'worker') {
event.respondWith(cacheFirst(request));
}
});
`;
}
function buildCacheVersion(isDemo: boolean, urls: string[]): string {
const digest = createHash('sha256')
.update(`${isDemo ? 'demo' : 'app'}\n${urls.join('\n')}`)
.digest('hex')
.slice(0, 16);
return `${isDemo ? 'demo' : 'app'}-${digest}`;
}
function pwaServiceWorkerPlugin(isDemo: boolean): Plugin {
return {
name: 'nodewarden-pwa-service-worker',
generateBundle(_, bundle) {
const urls = new Set([
'/',
'/index.html',
'/vault',
'/manifest.webmanifest',
'/nodewarden-logo.svg',
'/nodewarden-logo-bg.svg',
'/nodewarden-wordmark.svg',
'/favicon.ico',
'/favicon-32.png',
'/apple-touch-icon.png',
'/icon-192.png',
'/icon-512.png',
'/logo-64.png',
]);
const buildUrls = new Set(urls);
for (const [fileName, output] of Object.entries(bundle)) {
if (output.type !== 'chunk' && output.type !== 'asset') continue;
if (fileName === 'sw.js' || fileName === 'robots.txt') continue;
if (fileName.endsWith('.map')) continue;
buildUrls.add(`/${fileName}`);
}
const sortedUrls = Array.from(buildUrls).sort();
const version = buildCacheVersion(isDemo, Array.from(buildUrls).sort());
this.emitFile({
type: 'asset',
fileName: 'sw.js',
source: buildServiceWorkerSource(sortedUrls, version),
});
},
};
}
function searchIndexPolicyPlugin(isDemo: boolean): Plugin {
return {
name: 'nodewarden-search-index-policy',
transformIndexHtml(html: string) {
if (isDemo) return html;
return html.replace(
'',
'\n '
);
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'robots.txt',
source: isDemo
? 'User-agent: *\nAllow: /\n'
: 'User-agent: *\nDisallow: /\n',
});
},
};
}
function resourcePriorityPlugin(isDemo: boolean): Plugin {
return {
name: 'nodewarden-resource-priority',
enforce: 'post' as const,
transformIndexHtml(html: string) {
if (isDemo || !html.includes('/assets/app-suite-')) return html;
const scriptMatch = html.match(/^\s*