Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 80 additions & 9 deletions src/server/build-service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>([
"/",
"/_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"));
Comment on lines +56 to +58

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cache chunk assets with correct /_veryfront/chunks prefix

The service worker now reads chunk filenames from the build manifest, but both the entry JS (chunk.file) and its CSS are normalized with base "/_veryfront", producing cache URLs like /_veryfront/index.js. The chunk manifest produced by the code-splitter stores paths relative to the _veryfront/chunks outDir (see manifest-builder.ts where paths are derived via relative(outDir, file)), so the actual files are served under /_veryfront/chunks/<name>. With the current base, cache.addAll(STATIC_CACHE_URLS) will request non‑existent paths and the install step will fail whenever manifest entries omit the chunks/ prefix (the normal build output), leaving hashed bundles uncached. These paths should be prefixed with /_veryfront/chunks like the dependency handling below.

Useful? React with 👍 / 👎.

}
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 = {
Expand Down
65 changes: 56 additions & 9 deletions tests/integration/server/build-service-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ afterAll(async () => {
function createTestManifest(overrides?: Partial<BuildManifest>): BuildManifest {
return {
version: "2.0.0",
buildTime: new Date().toISOString(),
buildTime: "2024-01-01T00:00:00.000Z",
features: {
streaming: true,
codeSplitting: false,
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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());

Expand Down
Loading