feat: content-hash cache busting for module/shell assets
Build generates SHA-256 content hashes per asset file and appends ?v=<hash> to all module/shell URLs in rendered HTML automatically. Eliminates manual ?v=N bumping in mod.ts files. Versioned assets get Cache-Control: immutable headers, and the service worker cleans stale entries on activation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
feabf89137
commit
aff16e647a
|
|
@ -714,8 +714,8 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-calendar-view space="${space}"></folk-calendar-view>`,
|
||||
scripts: `<script type="module" src="/modules/rcal/folk-calendar-view.js?v=4"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rcal/cal.css?v=2">
|
||||
scripts: `<script type="module" src="/modules/rcal/folk-calendar-view.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rcal/cal.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">`,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-map-viewer space="${space}"></folk-map-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=3"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rmaps/folk-map-viewer.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css">`,
|
||||
}));
|
||||
});
|
||||
|
|
@ -158,7 +158,7 @@ routes.get("/:room", (c) => {
|
|||
theme: "dark",
|
||||
styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css">`,
|
||||
body: `<folk-map-viewer space="${space}" room="${room}"></folk-map-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=3"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rmaps/folk-map-viewer.js"></script>`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
body: `<div class="rapp-nav" style="padding:0 1rem;margin-top:8px"><span class="rapp-nav__title"></span><a href="?view=app" class="rapp-nav__btn--app-toggle">Open Full App</a></div>
|
||||
<folk-graph-viewer space="${space}"></folk-graph-viewer>`,
|
||||
scripts: `<script type="module" src="/modules/rnetwork/folk-graph-viewer.js?v=4"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rnetwork/folk-graph-viewer.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -764,8 +764,8 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-schedule-app space="${space}"></folk-schedule-app>`,
|
||||
scripts: `<script type="module" src="/modules/rschedule/folk-schedule-app.js?v=1"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rschedule/schedule.css?v=1">`,
|
||||
scripts: `<script type="module" src="/modules/rschedule/folk-schedule-app.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rschedule/schedule.css">`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -237,8 +237,8 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-swag-designer space="${space}"></folk-swag-designer>`,
|
||||
scripts: `<script type="module" src="/modules/rswag/folk-swag-designer.js?v=2"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rswag/swag.css?v=2">`,
|
||||
scripts: `<script type="module" src="/modules/rswag/folk-swag-designer.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rswag/swag.css">`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ routes.get("/", (c) => {
|
|||
modules: getModuleInfoList(),
|
||||
theme: "dark",
|
||||
body: `<folk-work-board space="${space}"></folk-work-board>`,
|
||||
scripts: `<script type="module" src="/modules/rwork/folk-work-board.js?v=3"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rwork/folk-work-board.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rwork/work.css">`,
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import type { ModuleInfo } from "../shared/module";
|
||||
import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS } from "./shell";
|
||||
import { escapeHtml, escapeAttr, MODULE_LANDING_CSS, RICH_LANDING_CSS, versionAssetUrls } from "./shell";
|
||||
|
||||
export function renderMainLanding(modules: ModuleInfo[]): string {
|
||||
const moduleListJSON = JSON.stringify(modules);
|
||||
|
|
@ -25,7 +25,7 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
|
|||
)
|
||||
.join("\n");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -34,8 +34,8 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
|
|||
<title>rSpace — Community Platform</title>
|
||||
<meta name="description" content="A collaborative, local-first community platform with 22+ interoperable tools. Design global, manufacture local.">
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=7">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>${MODULE_LANDING_CSS}</style>
|
||||
<style>${RICH_LANDING_CSS}</style>
|
||||
<style>${MAIN_LANDING_CSS}</style>
|
||||
|
|
@ -220,7 +220,7 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
|
|||
</footer>
|
||||
|
||||
<script type="module">
|
||||
import '/shell.js?v=7';
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
|
||||
// Logged-in users: hide header demo btn, swap hero CTA to "Go to My Space"
|
||||
|
|
@ -242,7 +242,7 @@ export function renderMainLanding(modules: ModuleInfo[]): string {
|
|||
} catch(e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
// ── Space Dashboard ──
|
||||
|
|
@ -267,7 +267,7 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri
|
|||
})
|
||||
.join("\n");
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -275,8 +275,8 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri
|
|||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||
<title>${escapeHtml(displayName)} | rSpace</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=7">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>${MODULE_LANDING_CSS}</style>
|
||||
<style>${SPACE_DASHBOARD_CSS}</style>
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||
|
|
@ -313,7 +313,7 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri
|
|||
</div>` : ""}
|
||||
|
||||
<script type="module">
|
||||
import '/shell.js?v=7';
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
|
||||
// Fix up dashboard links to be subdomain-aware
|
||||
|
|
@ -324,7 +324,7 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri
|
|||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
const SPACE_DASHBOARD_CSS = `
|
||||
|
|
|
|||
|
|
@ -5,9 +5,28 @@
|
|||
* switchers + identity, <main> with module content, shell script + styles.
|
||||
*/
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import type { ModuleInfo, SubPageInfo } from "../shared/module";
|
||||
import { getDocumentData } from "./community-store";
|
||||
|
||||
// ── Content-hash cache busting ──
|
||||
let moduleHashes: Record<string, string> = {};
|
||||
try {
|
||||
const hashFile = resolve(import.meta.dir, "../dist/module-hashes.json");
|
||||
moduleHashes = JSON.parse(await Bun.file(hashFile).text());
|
||||
} catch { /* dev mode or first run — no hashes yet */ }
|
||||
|
||||
/** Append ?v=<content-hash> to module/shell asset URLs in rendered HTML. */
|
||||
export function versionAssetUrls(html: string): string {
|
||||
return html.replace(
|
||||
/(["'])(\/(?:modules\/[^"'?]+\.(?:js|css)|shell\.js|shell\.css|theme\.css))(?:\?v=[^"']*)?(\1)/g,
|
||||
(_, q, path, qEnd) => {
|
||||
const hash = moduleHashes[path];
|
||||
return hash ? `${q}${path}?v=${hash}${qEnd}` : `${q}${path}${qEnd}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Extract enabledModules and encryption status from a loaded space. */
|
||||
export function getSpaceShellMeta(spaceSlug: string): { enabledModules: string[] | null; spaceEncrypted: boolean } {
|
||||
const data = getDocumentData(spaceSlug);
|
||||
|
|
@ -75,7 +94,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
const moduleListJSON = JSON.stringify(visibleModules);
|
||||
const shellDemoUrl = `https://demo.rspace.online/${escapeAttr(moduleId)}`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -83,8 +102,8 @@ export function renderShell(opts: ShellOptions): string {
|
|||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=8">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>
|
||||
/* When loaded inside an iframe (e.g. standalone domain redirecting back),
|
||||
hide the shell chrome — the parent rSpace page already provides it. */
|
||||
|
|
@ -128,7 +147,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
${renderWelcomeOverlay()}
|
||||
|
||||
<script type="module">
|
||||
import '/shell.js?v=8';
|
||||
import '/shell.js';
|
||||
// ── Settings panel toggle ──
|
||||
document.getElementById('settings-btn')?.addEventListener('click', () => {
|
||||
const panel = document.querySelector('rstack-space-settings');
|
||||
|
|
@ -498,7 +517,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
</script>
|
||||
${scripts}
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
// ── External app iframe shell ──
|
||||
|
|
@ -537,7 +556,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
const moduleListJSON = JSON.stringify(modules);
|
||||
const demoUrl = `?view=demo`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -545,8 +564,8 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🌌</text></svg>">
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=8">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>
|
||||
html.rspace-embedded .rstack-header { display: none !important; }
|
||||
html.rspace-embedded .rstack-tab-row { display: none !important; }
|
||||
|
|
@ -589,7 +608,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
</div>
|
||||
|
||||
<script type="module">
|
||||
import '/shell.js?v=8';
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
|
||||
const tabBar = document.querySelector('rstack-tab-bar');
|
||||
|
|
@ -621,7 +640,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
|||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
// ── Welcome overlay (quarter-screen popup for first-time visitors on demo) ──
|
||||
|
|
@ -791,7 +810,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
<a href="/">← Back to rSpace</a>
|
||||
</div>`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -799,8 +818,8 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${mod.icon}</text></svg>">
|
||||
<title>${escapeHtml(mod.name)} — rSpace</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=8">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
${cssBlock}
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||
</head>
|
||||
|
|
@ -821,7 +840,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
</header>
|
||||
${bodyContent}
|
||||
<script type="module">
|
||||
import '/shell.js?v=8';
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
function _updateDemoBtn() {
|
||||
var btn = document.querySelector('.rstack-header__demo-btn');
|
||||
|
|
@ -850,7 +869,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
|||
} catch(e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
export const MODULE_LANDING_CSS = `
|
||||
|
|
@ -1124,7 +1143,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
|||
<a href="/${escapeAttr(mod.id)}">← Back to ${escapeHtml(mod.name)}</a>
|
||||
</div>`;
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
return versionAssetUrls(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
|
@ -1132,8 +1151,8 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
|||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>${subPage.icon}</text></svg>">
|
||||
<title>${escapeHtml(subPage.title)} — ${escapeHtml(mod.name)} | rSpace</title>
|
||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
||||
<link rel="stylesheet" href="/theme.css?v=1">
|
||||
<link rel="stylesheet" href="/shell.css?v=8">
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<link rel="stylesheet" href="/shell.css">
|
||||
<style>${MODULE_LANDING_CSS}</style>
|
||||
<style>${RICH_LANDING_CSS}</style>
|
||||
<script defer src="https://rdata.online/collect.js" data-website-id="6ee7917b-0ed7-44cb-a4c8-91037638526b"></script>
|
||||
|
|
@ -1155,7 +1174,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
|||
</header>
|
||||
${bodyContent}
|
||||
<script type="module">
|
||||
import '/shell.js?v=8';
|
||||
import '/shell.js';
|
||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||
function _updateDemoBtn() {
|
||||
var btn = document.querySelector('.rstack-header__demo-btn');
|
||||
|
|
@ -1184,7 +1203,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
|||
} catch(e) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
</html>`);
|
||||
}
|
||||
|
||||
// ── Onboarding page (empty rApp state) ──
|
||||
|
|
|
|||
|
|
@ -935,9 +935,9 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
|
||||
// ── Generate precache manifest ──
|
||||
// Scans dist/ for all cacheable assets and writes precache-manifest.json
|
||||
const { readdirSync, writeFileSync, statSync: statSync2 } = await import("node:fs");
|
||||
// ── Generate content hashes for cache-busting ──
|
||||
const { readdirSync, readFileSync, writeFileSync, statSync: statSync2 } = await import("node:fs");
|
||||
const { createHash } = await import("node:crypto");
|
||||
const distDir = resolve(__dirname, "dist");
|
||||
|
||||
function walkDir(dir: string, prefix = ""): string[] {
|
||||
|
|
@ -955,6 +955,23 @@ export default defineConfig({
|
|||
|
||||
const allFiles = walkDir(distDir);
|
||||
|
||||
// Compute 8-char SHA-256 content hashes for module + shell assets
|
||||
const hashableFiles = allFiles.filter((f) =>
|
||||
(f.startsWith("/modules/") && (f.endsWith(".js") || f.endsWith(".css")) && !f.includes("-demo.js")) ||
|
||||
f === "/shell.js" || f === "/shell.css" || f === "/theme.css"
|
||||
);
|
||||
const hashes: Record<string, string> = {};
|
||||
for (const file of hashableFiles) {
|
||||
const content = readFileSync(resolve(distDir, file.slice(1)));
|
||||
hashes[file] = createHash("sha256").update(content).digest("hex").slice(0, 8);
|
||||
}
|
||||
writeFileSync(
|
||||
resolve(distDir, "module-hashes.json"),
|
||||
JSON.stringify(hashes, null, "\t"),
|
||||
);
|
||||
console.log(`[hashes] Generated content hashes for ${Object.keys(hashes).length} assets`);
|
||||
|
||||
// ── Generate precache manifest ──
|
||||
// Core: shell assets + HTML pages (precached at install, ~300KB)
|
||||
const core = allFiles.filter((f) =>
|
||||
f === "/" ||
|
||||
|
|
@ -966,21 +983,22 @@ export default defineConfig({
|
|||
f === "/shell.css" ||
|
||||
f === "/theme.css" ||
|
||||
f === "/favicon.png"
|
||||
);
|
||||
).map((f) => hashes[f] ? `${f}?v=${hashes[f]}` : f);
|
||||
// Ensure root URL is present
|
||||
if (!core.includes("/")) core.unshift("/");
|
||||
if (!core.some((f) => f === "/" || f.startsWith("/?v="))) core.unshift("/");
|
||||
|
||||
// Modules: all module JS + CSS (lazy-cached after activation)
|
||||
const modules = allFiles.filter((f) =>
|
||||
f.startsWith("/modules/") &&
|
||||
(f.endsWith(".js") || f.endsWith(".css")) &&
|
||||
!f.includes("-demo.js") // skip demo scripts
|
||||
);
|
||||
!f.includes("-demo.js")
|
||||
).map((f) => hashes[f] ? `${f}?v=${hashes[f]}` : f);
|
||||
|
||||
const manifest = {
|
||||
version: new Date().toISOString(),
|
||||
core,
|
||||
modules,
|
||||
hashes,
|
||||
};
|
||||
|
||||
writeFileSync(
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ const API_CACHE_MAX_AGE_MS = 5 * 60 * 1000;
|
|||
|
||||
interface PrecacheManifest {
|
||||
version: string;
|
||||
core: string[]; // shell assets + HTML pages — cached at install
|
||||
modules: string[]; // module JS/CSS — lazy-cached after activation
|
||||
core: string[]; // shell assets + HTML pages — cached at install (with ?v=hash)
|
||||
modules: string[]; // module JS/CSS — lazy-cached after activation (with ?v=hash)
|
||||
hashes?: Record<string, string>; // content hashes for cache-busting
|
||||
}
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
|
|
@ -74,6 +75,22 @@ self.addEventListener("activate", (event) => {
|
|||
if (!existing) await cache.add(url);
|
||||
} catch { /* skip individual failures */ }
|
||||
}
|
||||
|
||||
// Clean stale versioned entries (old ?v=oldhash URLs)
|
||||
const currentUrls = new Set([...manifest.core, ...manifest.modules]);
|
||||
const cachedRequests = await cache.keys();
|
||||
for (const req of cachedRequests) {
|
||||
const u = new URL(req.url);
|
||||
const isVersioned = u.searchParams.has("v") && (
|
||||
u.pathname.startsWith("/modules/") ||
|
||||
u.pathname === "/shell.js" ||
|
||||
u.pathname === "/shell.css" ||
|
||||
u.pathname === "/theme.css"
|
||||
);
|
||||
if (isVersioned && !currentUrls.has(u.pathname + u.search)) {
|
||||
await cache.delete(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { /* manifest unavailable */ }
|
||||
|
|
|
|||
Loading…
Reference in New Issue