fix(sw): stamp __TERMIX_SW_BASE_PATH__ and cache version into sw.js at build time (#1618)

* fix(sw): stamp __TERMIX_SW_BASE_PATH__ and version into sw.js at build time

vite copies public/ verbatim, so the placeholder in public/sw.js reached
dist/sw.js untouched. The service worker then requested
__TERMIX_SW_BASE_PATH__/favicon.ico and friends, its install promise
rejected, and it never activated - while a worker from an older release
kept serving its cached frontend against a newer backend.

scripts/stamp-sw-build-info.cjs now runs right after vite build and
replaces both placeholders in dist/sw.js:

- __TERMIX_SW_BASE_PATH__ -> the base path the build used (VITE_BASE_PATH,
  or root-relative by default, matching vite.config.ts)
- __TERMIX_CACHE_VERSION__ -> package.json version, so CACHE_NAME changes
  per release and the activate handler evicts the previous build's cache

Closes #1617

* chore: format stamp-sw-build-info.cjs to prettier style

* fix: preserve runtime service worker base paths

---------

Co-authored-by: inontz <inontz@users.noreply.github.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 08:17:40 +08:00
1 parent 8d9c83b0f6
commit e152bacc7c
5 files changed
+121 -3

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ COPY . .
RUN find public/fonts -name "*.ttf" ! -name "*Regular.ttf" ! -name "*Bold.ttf" ! -name "*Italic.ttf" -delete
RUN npm cache clean --force && \
NODE_OPTIONS="--max-old-space-size=3072" npm run build
TERMIX_RUNTIME_BASE_PATH=true NODE_OPTIONS="--max-old-space-size=3072" npm run build
# Stage 3: Build backend
FROM deps AS backend-builder
+1 -1
View File
@@ -30,7 +30,7 @@
"dev:electron": "node scripts/dev.mjs electron",
"dev:docker": "node scripts/dev.mjs docker",
"dev:frontend": "vite",
"build": "npm run build:sdk && vite build && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
"build": "npm run build:sdk && vite build && node scripts/stamp-sw-build-info.cjs && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
"build:backend": "npm run build:sdk && tsc -p tsconfig.node.json && node -e \"require('fs').copyFileSync('src/backend/package.json','dist/backend/package.json')\" && npm run build:plugins",
"generate:openapi": "npm run build:sdk && node scripts/generate-openapi.mjs",
"electron:rebuild": "electron-rebuild -f -o better-sqlite3,@serialport/bindings-cpp,node-pty",
+6 -1
View File
@@ -1,4 +1,9 @@
const CACHE_NAME = "termix-static-v2";
// CACHE_NAME carries the package version so a new release always gets a fresh
// cache: the activate handler below deletes every cache whose name differs
// from this one, which is what evicts a previous build's entries. Both
// placeholders on these lines are stamped by scripts/stamp-sw-build-info.cjs
// during `npm run build` - vite copies public/ verbatim and never transforms it.
const CACHE_NAME = "termix-static-" + "__TERMIX_CACHE_VERSION__";
const BASE_PATH = "__TERMIX_SW_BASE_PATH__";
const STATIC_ASSETS = [
`${BASE_PATH}/favicon.ico`,
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env node
/** Stamp the cache version and static-build base path.
* Docker keeps the base-path placeholder for entrypoint.sh to fill at startup.
*/
const fs = require("node:fs");
const path = require("node:path");
const ROOT = path.resolve(__dirname, "..");
const SW_PATH = path.join(ROOT, "dist", "sw.js");
const VERSION_PLACEHOLDER = "__TERMIX_CACHE_VERSION__";
const BASE_PATH_PLACEHOLDER = "__TERMIX_SW_BASE_PATH__";
const pkg = JSON.parse(
fs.readFileSync(path.join(ROOT, "package.json"), "utf8"),
);
const cacheVersion = String(pkg.version || "0.0.0").replace(/\./g, "-");
const rawBasePath = process.env.VITE_BASE_PATH || "./";
// "./" is what vite emits for a root deployment; cache URLs stay root-relative.
const basePath =
rawBasePath === "./"
? ""
: `/${rawBasePath.replace(/^\/+/, "").replace(/\/+$/, "")}`;
if (!fs.existsSync(SW_PATH)) {
console.error(`[stamp-sw] dist/sw.js not found - run vite build first`);
process.exit(1);
}
let sw = fs.readFileSync(SW_PATH, "utf8");
const rel = path.relative(ROOT, SW_PATH);
const replacements = [
[VERSION_PLACEHOLDER, cacheVersion],
...(process.env.TERMIX_RUNTIME_BASE_PATH === "true"
? []
: [[BASE_PATH_PLACEHOLDER, basePath]]),
];
let changed = false;
for (const [placeholder, value] of replacements) {
if (sw.includes(placeholder)) {
sw = sw.split(placeholder).join(value);
changed = true;
console.log(
`[stamp-sw] ${rel}: ${placeholder} -> ${JSON.stringify(value)}`,
);
}
}
if (changed) {
fs.writeFileSync(SW_PATH, sw);
}
+59
View File
@@ -0,0 +1,59 @@
import { execFileSync } from "node:child_process";
import {
mkdtempSync,
mkdirSync,
readFileSync,
writeFileSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { expect, it } from "vitest";
it.each([
[false, "", ""],
[false, "/termix/", "/termix"],
[true, "", "__TERMIX_SW_BASE_PATH__"],
])(
"stamps service worker paths (runtime=%s, base=%s)",
(runtime, base, expected) => {
const dir = mkdtempSync(path.join(tmpdir(), "termix-sw-"));
try {
mkdirSync(path.join(dir, "scripts"));
mkdirSync(path.join(dir, "dist"));
writeFileSync(
path.join(dir, "scripts/stamp-sw-build-info.cjs"),
readFileSync("scripts/stamp-sw-build-info.cjs"),
);
writeFileSync(
path.join(dir, "package.json"),
JSON.stringify({ version: "26.10.2" }),
);
writeFileSync(path.join(dir, "dist/sw.js"), readFileSync("public/sw.js"));
execFileSync(
process.execPath,
[path.join(dir, "scripts/stamp-sw-build-info.cjs")],
{
env: {
...process.env,
VITE_BASE_PATH: base,
TERMIX_RUNTIME_BASE_PATH: String(runtime),
},
},
);
const sw = readFileSync(path.join(dir, "dist/sw.js"), "utf8");
expect(sw).toContain('"26-10-2"');
expect(sw).toContain(`const BASE_PATH = "${expected}";`);
if (runtime) {
expect(sw.replaceAll("__TERMIX_SW_BASE_PATH__", "/termix")).toContain(
'const BASE_PATH = "/termix";',
);
expect(readFileSync("docker/Dockerfile", "utf8")).toContain(
"TERMIX_RUNTIME_BASE_PATH=true",
);
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);