fix(build): derive sw.js cache tag from bundle content hash

The service worker uses staleWhileRevalidate on /dist/*, so after a
release it would serve old bundles from cache indefinitely (cache is
only wiped when the cache *name* changes, which was hardcoded to
'dashcaddy-shell-v10'). Result: dashboard appears unchanged after a
self-update until the user manually unregisters the SW.

build.js now hashes the concatenated dist bundles and writes
`dashcaddy-shell-<10-hex-chars>` into sw.js. Any change in dist/
produces a fresh cache name; on the next page load the SW's activate
handler deletes all older caches and the new bundles are fetched.
This commit is contained in:
Sami
2026-05-17 01:41:47 -07:00
parent 3878509fc5
commit ebfc3be9d1
2 changed files with 26 additions and 2 deletions
+25 -1
View File
@@ -6,6 +6,7 @@ const esbuild = require('esbuild');
const JS = (...parts) => path.join(__dirname, 'js', ...parts); const JS = (...parts) => path.join(__dirname, 'js', ...parts);
const DIST = path.join(__dirname, 'dist'); const DIST = path.join(__dirname, 'dist');
const INDEX_HTML = path.join(__dirname, 'index.html'); const INDEX_HTML = path.join(__dirname, 'index.html');
const SW_JS = path.join(__dirname, 'sw.js');
// Bundle definitions — files are concatenated in order, then minified // Bundle definitions — files are concatenated in order, then minified
const bundles = { const bundles = {
@@ -129,6 +130,7 @@ async function build() {
} }
const cspHash = updateInlineScriptCspHash(); const cspHash = updateInlineScriptCspHash();
const cacheTag = updateServiceWorkerCache();
// Summary // Summary
console.log('\n DashCaddy Frontend Build\n'); console.log('\n DashCaddy Frontend Build\n');
@@ -143,7 +145,29 @@ async function build() {
console.log(' ─────────────────────────────────────────'); console.log(' ─────────────────────────────────────────');
console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`); console.log(` ${'Total'.padEnd(18)} ${totalRaw.toFixed(1).padStart(6)} KB ${totalMin.toFixed(1).padStart(6)} KB`);
console.log(`\n Output: ${DIST}`); console.log(`\n Output: ${DIST}`);
console.log(` CSP Hash: sha256-${cspHash}\n`); console.log(` CSP Hash: sha256-${cspHash}`);
console.log(` SW Cache: dashcaddy-shell-${cacheTag}\n`);
}
// Rewrites the CACHE constant in sw.js to a tag derived from the bundle
// contents. Every change in dist/ produces a new cache name; on next load
// the SW's activate handler wipes all older caches, so users never get
// stuck on stale precached bundles after a release.
function updateServiceWorkerCache() {
const sw = fs.readFileSync(SW_JS, 'utf8');
const hash = crypto.createHash('sha256');
for (const name of Object.keys(bundles)) {
hash.update(fs.readFileSync(path.join(DIST, name)));
}
const tag = hash.digest('hex').slice(0, 10);
const updated = sw.replace(
/const CACHE = 'dashcaddy-shell-[^']+';/,
`const CACHE = 'dashcaddy-shell-${tag}';`
);
if (updated !== sw) {
fs.writeFileSync(SW_JS, updated);
}
return tag;
} }
// Watch mode // Watch mode
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'dashcaddy-shell-v10'; const CACHE = 'dashcaddy-shell-c6600af35d';
const PRECACHE = [ const PRECACHE = [
'/', '/',
'/index.html', '/index.html',