From 60f587c5a7e74fda1492cdca346fccf9334528b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 14 Jun 2026 10:47:50 +0200 Subject: [PATCH] fix(pwa): re-anchor the sw.js fallback version on every build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `public/sw.js` carries a `|| "..."` fallback literal used for `CACHE_VERSION` when `importScripts('/sw-version.js')` is unavailable. The literal had drifted to `v1.4.43`, four-plus minors behind `package.json`; in any environment where the generated file is missing this keyed every cache to the stale version and turned the activate-step eviction into a no-op — the exact bug the generator was written to kill. Extend the prebuild generator to rewrite the fallback literal in `sw.js` to the current `package.json` version on every build, anchored on a `/* @sw-version-fallback */` marker (the build fails loudly if the marker is gone). The literal and the generated `sw-version.js` can no longer diverge. The SW test now reads the fallback straight from the source so it tracks the literal instead of hard-coding it. --- public/sw.js | 10 +++++++++- scripts/generate-sw-version.mjs | 21 ++++++++++++++++++++- src/__tests__/service-worker.test.ts | 14 ++++++++++---- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/public/sw.js b/public/sw.js index c85334d78..2e4cbf415 100644 --- a/public/sw.js +++ b/public/sw.js @@ -27,8 +27,16 @@ try { } catch { // Fall through to the literal fallback below. } +// The `|| "..."` fallback literal is the value used only when the +// `importScripts('/sw-version.js')` above is absent (dev with no `prebuild` +// step, or a 404 on the generated file). `scripts/generate-sw-version.mjs` +// rewrites it to the current `package.json` version on every prebuild — the +// marker below is the rewrite anchor — so in any shipped image the literal +// matches the active release and can never drift stale the way it did across +// v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = - (typeof self !== "undefined" && self.__APP_VERSION__) || "v1.4.43"; + (typeof self !== "undefined" && self.__APP_VERSION__) || + /* @sw-version-fallback */ "v1.16.12"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; const MAX_STATIC_ENTRIES = 150; diff --git a/scripts/generate-sw-version.mjs b/scripts/generate-sw-version.mjs index 5157f051b..b206edad8 100644 --- a/scripts/generate-sw-version.mjs +++ b/scripts/generate-sw-version.mjs @@ -23,6 +23,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(__dirname, ".."); const pkgPath = join(repoRoot, "package.json"); const outPath = join(repoRoot, "public", "sw-version.js"); +const swPath = join(repoRoot, "public", "sw.js"); const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); const version = `v${pkg.version}`; @@ -33,5 +34,23 @@ const body = `self.__APP_VERSION__ = ${JSON.stringify(version)};\n`; writeFileSync(outPath, banner + body, "utf8"); +// Also re-anchor the in-`sw.js` fallback literal to the current version, so +// the literal used when `/sw-version.js` is missing (dev, or a 404 on the +// generated file) can never drift stale — it bit v1.4.38.4 → v1.4.42. The +// `/* @sw-version-fallback */` marker is the rewrite anchor; the literal that +// follows it is replaced in place. If the marker is gone the SW source has +// been restructured and the build must surface that rather than silently skip +// the re-anchor. +const sw = readFileSync(swPath, "utf8"); +const fallbackPattern = /(\/\* @sw-version-fallback \*\/\s*)"v[^"]*"/; +if (!fallbackPattern.test(sw)) { + process.stderr.write( + "generate-sw-version: @sw-version-fallback marker not found in public/sw.js\n", + ); + process.exit(1); +} +const rewritten = sw.replace(fallbackPattern, `$1${JSON.stringify(version)}`); +if (rewritten !== sw) writeFileSync(swPath, rewritten, "utf8"); + // One-line log so the build output shows the version we baked in. -process.stdout.write(`sw-version.js → ${version}\n`); +process.stdout.write(`sw-version.js → ${version} (sw.js fallback re-anchored)\n`); diff --git a/src/__tests__/service-worker.test.ts b/src/__tests__/service-worker.test.ts index 8601c0a41..cde471c89 100644 --- a/src/__tests__/service-worker.test.ts +++ b/src/__tests__/service-worker.test.ts @@ -24,10 +24,16 @@ const SW_SOURCE = readFileSync( ); const ORIGIN = "https://app.example"; -// `importScripts` throws in the sandbox, so the literal fallback version -// is active and the cache names are deterministic. -const CURRENT_PAGE_CACHE = "healthlog-pages-v1.4.43"; -const CURRENT_STATIC_CACHE = "healthlog-static-v1.4.43"; +// `importScripts` throws in the sandbox, so the in-`sw.js` fallback literal +// is active and the cache names are deterministic. The fallback is +// re-anchored to `package.json` on every prebuild by +// `scripts/generate-sw-version.mjs`; read it straight from the SW source so +// this test never drifts from the literal it is exercising. +const FALLBACK_VERSION = /\/\* @sw-version-fallback \*\/\s*"(v[^"]*)"/.exec( + SW_SOURCE, +)![1]; +const CURRENT_PAGE_CACHE = `healthlog-pages-${FALLBACK_VERSION}`; +const CURRENT_STATIC_CACHE = `healthlog-static-${FALLBACK_VERSION}`; class FakeCache { map = new Map();