diff --git a/src/server/build-service-worker.ts b/src/server/build-service-worker.ts index 7aad0c7c56..7289c287e5 100644 --- a/src/server/build-service-worker.ts +++ b/src/server/build-service-worker.ts @@ -4,24 +4,95 @@ import type { BuildManifest } from "../build/production-build/manifest.ts"; +function sanitizeCacheKey(value: string): string { + return value.replace(/[^a-zA-Z0-9._-]/g, ""); +} + +function buildCacheVersion(manifest: BuildManifest): string { + const manifestVersion = sanitizeCacheKey(manifest.version || "dev"); + const buildStamp = sanitizeCacheKey(manifest.buildTime || new Date().toISOString()); + return `veryfront-${manifestVersion}-${buildStamp}`; +} + +function normalizeChunkPath(value: string | null | undefined, base: string): string | null { + if (!value) return null; + if (value.startsWith("http://") || value.startsWith("https://")) return null; + + const candidate = value.replace(/^\.\//, ""); + + if (candidate.startsWith("/")) { + return candidate; + } + + if (candidate.startsWith("_veryfront/")) { + return `/${candidate}`; + } + + if (candidate.startsWith("chunks/")) { + return `/_veryfront/${candidate}`; + } + + return `${base}/${candidate}`; +} + +function buildManifestAssets(manifest: BuildManifest): string[] { + const assets = new Set([ + "/", + "/_veryfront/router.js", + "/_veryfront/prefetch.js", + "/_veryfront/manifest.json", + "/sw.js", + ]); + + const addAsset = (requestPath: string | null | undefined) => { + if (!requestPath) return; + const normalized = requestPath.startsWith("/") ? requestPath : `/${requestPath}`; + assets.add(normalized); + }; + + if (manifest.chunks) { + for (const chunkInfo of Object.values(manifest.chunks.chunks || {})) { + const chunk = chunkInfo as any; + addAsset(normalizeChunkPath(chunk.file, "/_veryfront")); + if (chunk.css) { + addAsset(normalizeChunkPath(chunk.css, "/_veryfront")); + } + for (const dependency of chunk.imports || []) { + addAsset(normalizeChunkPath(dependency, "/_veryfront/chunks")); + } + } + + for (const shared of manifest.chunks.shared || []) { + addAsset(normalizeChunkPath(shared, "/_veryfront/chunks")); + } + } + + for (const route of manifest.routes || []) { + if (Array.isArray(route.chunks)) { + for (const chunk of route.chunks) { + addAsset(normalizeChunkPath(chunk, "/_veryfront/chunks")); + } + } + } + + return Array.from(assets).sort(); +} + /** * Generate service worker with advanced caching */ -export function generateServiceWorker(_manifest: BuildManifest): string { +export function generateServiceWorker(manifest: BuildManifest): string { + const cacheVersion = buildCacheVersion(manifest); + const staticAssets = buildManifestAssets(manifest); + return `// Veryfront Service Worker // Generated at: ${new Date().toISOString()} -const CACHE_VERSION = 'veryfront-v2'; +const CACHE_VERSION = '${cacheVersion}'; const RUNTIME_CACHE = 'veryfront-runtime'; // Static resources to cache -const STATIC_CACHE_URLS = [ - '/', - '/_veryfront/router.js', - '/_veryfront/prefetch.js', - '/_veryfront/manifest.json', - '/sw.js', -]; +const STATIC_CACHE_URLS = ${JSON.stringify(staticAssets, null, 2)}; // Cache strategies const CACHE_STRATEGIES = { diff --git a/tests/integration/server/build-service-worker.test.ts b/tests/integration/server/build-service-worker.test.ts index fceba10618..0f96b3087b 100644 --- a/tests/integration/server/build-service-worker.test.ts +++ b/tests/integration/server/build-service-worker.test.ts @@ -13,7 +13,7 @@ afterAll(async () => { function createTestManifest(overrides?: Partial): BuildManifest { return { version: "2.0.0", - buildTime: new Date().toISOString(), + buildTime: "2024-01-01T00:00:00.000Z", features: { streaming: true, codeSplitting: false, @@ -46,9 +46,10 @@ describe("Service Worker Generation", () => { }); it("should include cache version constant", () => { - const code = generateServiceWorker(createTestManifest()); + const manifest = createTestManifest(); + const code = generateServiceWorker(manifest); - assert(code.includes("const CACHE_VERSION = 'veryfront-v2'")); + assert(code.includes("const CACHE_VERSION = 'veryfront-2.0.0-2024-01-01T000000.000Z'")); }); it("should include runtime cache constant", () => { @@ -61,11 +62,38 @@ describe("Service Worker Generation", () => { const code = generateServiceWorker(createTestManifest()); assert(code.includes("STATIC_CACHE_URLS")); - assert(code.includes("'/'")); - assert(code.includes("'/_veryfront/router.js'")); - assert(code.includes("'/_veryfront/prefetch.js'")); - assert(code.includes("'/_veryfront/manifest.json'")); - assert(code.includes("'/sw.js'")); + assert(code.includes("\"/\"")); + assert(code.includes("\"/_veryfront/router.js\"")); + assert(code.includes("\"/_veryfront/prefetch.js\"")); + assert(code.includes("\"/_veryfront/manifest.json\"")); + assert(code.includes("\"/sw.js\"")); + }); + + it("should include manifest assets in static cache", () => { + const manifest = createTestManifest({ + routes: [ + { path: "/", slug: "index", chunks: ["chunks/home-abc123.js"] }, + ], + chunks: { + version: "1", + routes: { "/": { chunks: ["chunks/home-abc123.js"] } }, + chunks: { + "chunks/home-abc123.js": { + file: "chunks/home-abc123.js", + css: "chunks/home-abc123.css", + imports: ["chunks/vendor-xyz.js"], + }, + }, + shared: ["chunks/shared-1.js"], + }, + }); + + const code = generateServiceWorker(manifest); + + assert(code.includes("\"/_veryfront/chunks/home-abc123.js\"")); + assert(code.includes("\"/_veryfront/chunks/home-abc123.css\"")); + assert(code.includes("\"/_veryfront/chunks/vendor-xyz.js\"")); + assert(code.includes("\"/_veryfront/chunks/shared-1.js\"")); }); it("should define cache strategies object", () => { @@ -404,13 +432,32 @@ describe("Service Worker Generation", () => { const code = generateServiceWorker(createTestManifest()); // Should have versioned cache - assert(code.includes("const CACHE_VERSION = 'veryfront-v2'")); + assert(code.includes("const CACHE_VERSION = 'veryfront-2.0.0-2024-01-01T000000.000Z'")); // Should use it for static cache assert(code.includes("caches.open(CACHE_VERSION)")); // Version should be used in cleanup logic assert(code.includes("name !== CACHE_VERSION")); }); + it("should bump cache version when manifest changes", () => { + const extractCacheVersion = (source: string) => { + const match = source.match(/const CACHE_VERSION = '([^']+)'/); + return match?.[1] ?? null; + }; + + const firstManifest = createTestManifest({ buildTime: "2024-01-01T00:00:00.000Z" }); + const secondManifest = createTestManifest({ buildTime: "2024-02-02T00:00:00.000Z" }); + + const firstCode = generateServiceWorker(firstManifest); + const secondCode = generateServiceWorker(secondManifest); + + const firstVersion = extractCacheVersion(firstCode); + const secondVersion = extractCacheVersion(secondCode); + + assert(firstVersion && secondVersion); + assert(firstVersion !== secondVersion); + }); + it("should store responses only when response.ok is true", () => { const code = generateServiceWorker(createTestManifest());