mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(pwa): re-anchor SW CACHE_VERSION on every build via prebuild step
This commit is contained in:
1 parent
7c3d567430
commit
c0a2c8784e
5 files changed
+131
-1
No files matched your search
@@ -49,6 +49,11 @@ next-env.d.ts
|
||||
# prisma generated
|
||||
/src/generated/prisma
|
||||
|
||||
# v1.4.43 QoL (L3) — generated at build time by
|
||||
# scripts/generate-sw-version.mjs; loaded by public/sw.js to key
|
||||
# the cache-version baseline.
|
||||
/public/sw-version.js
|
||||
|
||||
# v1.4.27 B3 — MaxMind GeoLite2 databases are fetched at build time
|
||||
# via `scripts/fetch-geolite2.sh`. Too large for git; license key
|
||||
# required. The folder + a `.gitkeep` are tracked so the Dockerfile
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"packageManager": "pnpm@10.31.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"prebuild": "node scripts/generate-sw-version.mjs",
|
||||
"build": "next build",
|
||||
"analyze": "ANALYZE=1 next build",
|
||||
"bundle-report": "node scripts/print-bundle-report.mjs",
|
||||
|
||||
+16
-1
@@ -13,7 +13,22 @@
|
||||
// polls `/api/version`, compares against `NEXT_PUBLIC_APP_VERSION`,
|
||||
// and triggers an SW-unregister + cache-wipe + hard reload when the
|
||||
// server moves ahead of the running shell.
|
||||
const CACHE_VERSION = "v1.4.38.4";
|
||||
//
|
||||
// v1.4.43 QoL (L3) — read the active version from `self.__APP_VERSION__`,
|
||||
// which the build step writes to `/public/sw-version.js` (loaded
|
||||
// synchronously below via `importScripts`). Pre-fix the literal was
|
||||
// hand-bumped per release and quietly drifted four releases stale, so
|
||||
// every deploy between v1.4.38.4 → v1.4.42 served the previous shell's
|
||||
// cache instead of evicting it on activate. The fallback literal stays
|
||||
// in case the import is missing (legacy SW versions, dev mode without
|
||||
// the build step), but the source of truth is now the generated file.
|
||||
try {
|
||||
importScripts("/sw-version.js");
|
||||
} catch {
|
||||
// Fall through to the literal fallback below.
|
||||
}
|
||||
const CACHE_VERSION =
|
||||
(typeof self !== "undefined" && self.__APP_VERSION__) || "v1.4.43";
|
||||
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
|
||||
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
|
||||
const MAX_STATIC_ENTRIES = 150;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* v1.4.43 QoL (L3) — verify the SW build step + the SW source contract.
|
||||
*
|
||||
* Pre-fix `public/sw.js` hard-coded `const CACHE_VERSION = "v1.4.38.4"`
|
||||
* which drifted four releases stale. The current shape:
|
||||
* - `scripts/generate-sw-version.mjs` writes `self.__APP_VERSION__`
|
||||
* into `public/sw-version.js` from `package.json`.
|
||||
* - `public/sw.js` `importScripts('/sw-version.js')` and reads
|
||||
* `self.__APP_VERSION__` (falling back to a literal).
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, it, expect, afterAll } from "vitest";
|
||||
|
||||
const ROOT = join(__dirname, "../..");
|
||||
const SW_PATH = join(ROOT, "public/sw.js");
|
||||
const GENERATED_PATH = join(ROOT, "public/sw-version.js");
|
||||
const SCRIPT_PATH = join(ROOT, "scripts/generate-sw-version.mjs");
|
||||
const PKG_PATH = join(ROOT, "package.json");
|
||||
|
||||
describe("public/sw.js CACHE_VERSION wiring", () => {
|
||||
it("does not carry a hard-coded pre-v1.4.43 literal", () => {
|
||||
const sw = readFileSync(SW_PATH, "utf8");
|
||||
// The exact pre-fix literal — pin out a regression where someone
|
||||
// re-hand-codes the version instead of going through the script.
|
||||
expect(sw).not.toContain('const CACHE_VERSION = "v1.4.38.4"');
|
||||
// The fix routes via `self.__APP_VERSION__`.
|
||||
expect(sw).toContain("self.__APP_VERSION__");
|
||||
});
|
||||
|
||||
it("loads the generated version file via importScripts", () => {
|
||||
const sw = readFileSync(SW_PATH, "utf8");
|
||||
expect(sw).toContain("importScripts");
|
||||
expect(sw).toContain("/sw-version.js");
|
||||
});
|
||||
|
||||
it("falls back to a literal when the import fails", () => {
|
||||
const sw = readFileSync(SW_PATH, "utf8");
|
||||
// The fallback OR-expression keeps the SW functional in dev mode
|
||||
// where the build script has not run. Pin the shape so the fallback
|
||||
// can never silently vanish.
|
||||
expect(sw).toMatch(/self\.__APP_VERSION__\s*\)?\s*\|\|\s*"v\d/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("scripts/generate-sw-version.mjs", () => {
|
||||
afterAll(() => {
|
||||
// Don't leave the generated artefact behind for the next test run.
|
||||
if (existsSync(GENERATED_PATH)) rmSync(GENERATED_PATH);
|
||||
});
|
||||
|
||||
it("writes self.__APP_VERSION__ matching package.json", () => {
|
||||
execFileSync("node", [SCRIPT_PATH], { stdio: "pipe" });
|
||||
expect(existsSync(GENERATED_PATH)).toBe(true);
|
||||
const pkg = JSON.parse(readFileSync(PKG_PATH, "utf8"));
|
||||
const generated = readFileSync(GENERATED_PATH, "utf8");
|
||||
expect(generated).toContain(`self.__APP_VERSION__ = "v${pkg.version}"`);
|
||||
});
|
||||
|
||||
it("re-running the script is idempotent (overwrites with same content)", () => {
|
||||
execFileSync("node", [SCRIPT_PATH], { stdio: "pipe" });
|
||||
const a = readFileSync(GENERATED_PATH, "utf8");
|
||||
execFileSync("node", [SCRIPT_PATH], { stdio: "pipe" });
|
||||
const b = readFileSync(GENERATED_PATH, "utf8");
|
||||
expect(b).toBe(a);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* v1.4.43 QoL (L3) — write the active package.json version into
|
||||
* `public/sw-version.js`. The Service Worker (`public/sw.js`) loads
|
||||
* this file via `importScripts('/sw-version.js')` and reads
|
||||
* `self.__APP_VERSION__` to key its `CACHE_VERSION`.
|
||||
*
|
||||
* Pre-fix the SW carried a hand-bumped literal that drifted four
|
||||
* releases stale; the `activate`-step's old-cache eviction became
|
||||
* a no-op for every deploy in v1.4.38.4 → v1.4.42. Running this
|
||||
* script as a `prebuild` step re-anchors the version on every build,
|
||||
* so the eviction now fires reliably.
|
||||
*
|
||||
* Idempotent: writes the file unconditionally. Safe to commit the
|
||||
* generated file or `.gitignore` it — either way `pnpm build` reruns
|
||||
* the generator.
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
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 pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
||||
const version = `v${pkg.version}`;
|
||||
|
||||
const banner =
|
||||
"// AUTO-GENERATED by scripts/generate-sw-version.mjs — do not edit.\n";
|
||||
const body = `self.__APP_VERSION__ = ${JSON.stringify(version)};\n`;
|
||||
|
||||
writeFileSync(outPath, banner + body, "utf8");
|
||||
|
||||
// One-line log so the build output shows the version we baked in.
|
||||
process.stdout.write(`sw-version.js → ${version}\n`);
|
||||
Reference in new issue
Block a user