fix stale frontend cache in install packages

This commit is contained in:
2026-05-20 02:56:44 +08:00
parent 35cd438018
commit 93aa73ab5e
8 changed files with 389 additions and 8 deletions

6
dist/SHA256SUMS vendored
View File

@@ -1,3 +1,3 @@
4310b841d283d76465e8f7c53d630b24ad10e75ae5b6039399db1adcded8702e reactive-resume-clean-install-20260520.zip 1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
c2a24e404fe47bf4c21c01d22fa21af0c86323ef246606fe7152ad293b46ab34 reactive-resume-personal-direct-20260520.zip 34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
5d26c24423159df199b9c2263d02de64912d7c4f44f7e17bf5beef2d2d601e8f reactive-resume-personal-qnap-nas-20260520.zip 933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip

View File

@@ -1,3 +1,3 @@
4310b841d283d76465e8f7c53d630b24ad10e75ae5b6039399db1adcded8702e reactive-resume-clean-install-20260520.zip 1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
c2a24e404fe47bf4c21c01d22fa21af0c86323ef246606fe7152ad293b46ab34 reactive-resume-personal-direct-20260520.zip 34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
5d26c24423159df199b9c2263d02de64912d7c4f44f7e17bf5beef2d2d601e8f reactive-resume-personal-qnap-nas-20260520.zip 933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip

Binary file not shown.

Binary file not shown.

View File

@@ -62,7 +62,21 @@ const explicitSsrFile = process.env.SSR_FILE || "";
const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs"); const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs");
const filenameCacheBust = "rr-filename-title-20260520b"; const filenameCacheBust = "rr-filename-title-20260520b";
const pdfCacheBust = "rr-glalie-layout-20260520"; const pdfCacheBust = "rr-glalie-layout-20260520";
const appShellSuffix = "rr20260520c";
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */"; const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
const serviceWorkerCleanup = `/* Reactive Resume personal deployment: disable stale PWA caches. */
self.addEventListener("install", (event) => {
self.skipWaiting();
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key)))));
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
await caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key))));
await self.clients.claim();
})());
});
self.addEventListener("fetch", () => {});
`;
function warn(message) { function warn(message) {
console.warn(`Reactive Resume runtime patch: ${message}`); console.warn(`Reactive Resume runtime patch: ${message}`);
@@ -111,6 +125,20 @@ function listJsFiles(dir) {
.map((name) => path.join(dir, name)); .map((name) => path.join(dir, name));
} }
function listFilesRecursive(dir, predicate) {
if (!fs.existsSync(dir)) return [];
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...listFilesRecursive(file, predicate));
} else if (predicate(file)) {
result.push(file);
}
}
return result;
}
function replaceOnce(source, from, to) { function replaceOnce(source, from, to) {
return source.includes(to) ? source : source.replace(from, to); return source.includes(to) ? source : source.replace(from, to);
} }
@@ -185,6 +213,79 @@ function patchSsr(source) {
return source; return source;
} }
function patchAppManifest(file, oldBase, newBase) {
if (!fs.existsSync(file)) return false;
let source = read(file);
const next = source.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
if (next !== source) {
write(file, next);
return true;
}
return false;
}
function cloneStaticManifestEntry(source, oldUrl, newUrl, oldBase, newBase, newFile) {
const marker = `\t"${oldUrl}": {`;
const start = source.indexOf(marker);
if (start === -1) {
warn(`static manifest entry not found for ${oldUrl}, skipped app shell cache bust`);
return source;
}
if (source.includes(`\t"${newUrl}": {`)) {
return patchStaticManifestEntry(source, newUrl, newFile);
}
const close = source.indexOf("\n\t}", start);
if (close === -1 || source[close + 3] !== ",") {
warn(`static manifest entry close marker not found for ${oldUrl}, skipped app shell cache bust`);
return source;
}
let entry = source.slice(start, close + 3);
const buffer = fs.readFileSync(newFile);
entry = entry
.replace(oldUrl, newUrl)
.replace(`../public/assets/${oldBase}`, `../public/assets/${newBase}`)
.replace(/"etag": "(?:\\.|[^"\\])*"/, `"etag": ${JSON.stringify(makeEtag(buffer))}`)
.replace(/"mtime": "(?:\\.|[^"\\])*"/, `"mtime": ${JSON.stringify(new Date().toISOString())}`)
.replace(/"size": \d+/, `"size": ${buffer.length}`);
return source.slice(0, close + 4) + "\n" + entry + "," + source.slice(close + 4);
}
function patchAppShellEntry(assetFiles) {
const indexFile = assetFiles.find((file) => /^index-[A-Za-z0-9_-]+\.js$/.test(path.basename(file)));
if (!indexFile) {
warn("index app shell bundle not found, skipped app shell cache bust");
return null;
}
const oldBase = path.basename(indexFile);
const newBase = oldBase.replace(/\.js$/, `-${appShellSuffix}.js`);
const newFile = path.join(path.dirname(indexFile), newBase);
if (!fs.existsSync(newFile)) {
fs.copyFileSync(indexFile, newFile);
}
return { oldBase, newBase, newFile };
}
function patchAppShellImporters(assetFiles, oldBase, newBase) {
const touched = [];
for (const file of assetFiles) {
if (path.basename(file) === oldBase) continue;
let source = read(file);
let next = source
.replace(new RegExp(`\\./${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `./${newBase}`)
.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
if (next !== source) {
write(file, next);
touched.push(file);
}
}
return touched;
}
function patchPublicPdf(source) { function patchPublicPdf(source) {
if (!source.includes("rr-browser-buffer-polyfill")) { if (!source.includes("rr-browser-buffer-polyfill")) {
const importPrelude = source.match(/^(?:import[^;]+;)+/); const importPrelude = source.match(/^(?:import[^;]+;)+/);
@@ -241,6 +342,16 @@ if (ssrFile) {
warn("SSR bundle with generateFilename not found"); warn("SSR bundle with generateFilename not found");
} }
const appShell = patchAppShellEntry(assetFiles);
const appShellTouchedImporters = appShell ? patchAppShellImporters(assetFiles, appShell.oldBase, appShell.newBase) : [];
if (appShell) {
const appManifestFiles = [
...listFilesRecursive(path.join(outputDir, "server"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
...listFilesRecursive(path.join(appDir, "apps/server/dist"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
];
for (const file of appManifestFiles) patchAppManifest(file, appShell.oldBase, appShell.newBase);
}
const pdfFile = assetFiles const pdfFile = assetFiles
.filter((file) => path.basename(file).startsWith("pdf-document-")) .filter((file) => path.basename(file).startsWith("pdf-document-"))
.sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || ""; .sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || "";
@@ -270,9 +381,24 @@ for (const file of assetFiles) {
if (fs.existsSync(serverIndexFile)) { if (fs.existsSync(serverIndexFile)) {
let serverIndex = read(serverIndexFile); let serverIndex = read(serverIndexFile);
for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters].filter(Boolean)) { for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters, ...appShellTouchedImporters].filter(Boolean)) {
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file); serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file);
} }
if (appShell) {
serverIndex = cloneStaticManifestEntry(
serverIndex,
`/assets/${appShell.oldBase}`,
`/assets/${appShell.newBase}`,
appShell.oldBase,
appShell.newBase,
appShell.newFile,
);
}
const serviceWorkerFile = path.join(path.dirname(assetsDir), "sw.js");
if (fs.existsSync(serviceWorkerFile)) {
write(serviceWorkerFile, serviceWorkerCleanup);
serverIndex = patchStaticManifestEntry(serverIndex, "/sw.js", serviceWorkerFile);
}
write(serverIndexFile, serverIndex); write(serverIndexFile, serverIndex);
} }

View File

@@ -62,7 +62,21 @@ const explicitSsrFile = process.env.SSR_FILE || "";
const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs"); const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs");
const filenameCacheBust = "rr-filename-title-20260520b"; const filenameCacheBust = "rr-filename-title-20260520b";
const pdfCacheBust = "rr-glalie-layout-20260520"; const pdfCacheBust = "rr-glalie-layout-20260520";
const appShellSuffix = "rr20260520c";
const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */"; const browserBufferPolyfill = "var Buffer=globalThis.Buffer??{isBuffer:()=>false,allocUnsafe:e=>new Uint8Array(e),alloc:e=>new Uint8Array(e)};/* rr-browser-buffer-polyfill */";
const serviceWorkerCleanup = `/* Reactive Resume personal deployment: disable stale PWA caches. */
self.addEventListener("install", (event) => {
self.skipWaiting();
event.waitUntil(caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key)))));
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
await caches.keys().then((keys) => Promise.all(keys.map((key) => caches.delete(key))));
await self.clients.claim();
})());
});
self.addEventListener("fetch", () => {});
`;
function warn(message) { function warn(message) {
console.warn(`Reactive Resume runtime patch: ${message}`); console.warn(`Reactive Resume runtime patch: ${message}`);
@@ -111,6 +125,20 @@ function listJsFiles(dir) {
.map((name) => path.join(dir, name)); .map((name) => path.join(dir, name));
} }
function listFilesRecursive(dir, predicate) {
if (!fs.existsSync(dir)) return [];
const result = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const file = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...listFilesRecursive(file, predicate));
} else if (predicate(file)) {
result.push(file);
}
}
return result;
}
function replaceOnce(source, from, to) { function replaceOnce(source, from, to) {
return source.includes(to) ? source : source.replace(from, to); return source.includes(to) ? source : source.replace(from, to);
} }
@@ -185,6 +213,79 @@ function patchSsr(source) {
return source; return source;
} }
function patchAppManifest(file, oldBase, newBase) {
if (!fs.existsSync(file)) return false;
let source = read(file);
const next = source.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
if (next !== source) {
write(file, next);
return true;
}
return false;
}
function cloneStaticManifestEntry(source, oldUrl, newUrl, oldBase, newBase, newFile) {
const marker = `\t"${oldUrl}": {`;
const start = source.indexOf(marker);
if (start === -1) {
warn(`static manifest entry not found for ${oldUrl}, skipped app shell cache bust`);
return source;
}
if (source.includes(`\t"${newUrl}": {`)) {
return patchStaticManifestEntry(source, newUrl, newFile);
}
const close = source.indexOf("\n\t}", start);
if (close === -1 || source[close + 3] !== ",") {
warn(`static manifest entry close marker not found for ${oldUrl}, skipped app shell cache bust`);
return source;
}
let entry = source.slice(start, close + 3);
const buffer = fs.readFileSync(newFile);
entry = entry
.replace(oldUrl, newUrl)
.replace(`../public/assets/${oldBase}`, `../public/assets/${newBase}`)
.replace(/"etag": "(?:\\.|[^"\\])*"/, `"etag": ${JSON.stringify(makeEtag(buffer))}`)
.replace(/"mtime": "(?:\\.|[^"\\])*"/, `"mtime": ${JSON.stringify(new Date().toISOString())}`)
.replace(/"size": \d+/, `"size": ${buffer.length}`);
return source.slice(0, close + 4) + "\n" + entry + "," + source.slice(close + 4);
}
function patchAppShellEntry(assetFiles) {
const indexFile = assetFiles.find((file) => /^index-[A-Za-z0-9_-]+\.js$/.test(path.basename(file)));
if (!indexFile) {
warn("index app shell bundle not found, skipped app shell cache bust");
return null;
}
const oldBase = path.basename(indexFile);
const newBase = oldBase.replace(/\.js$/, `-${appShellSuffix}.js`);
const newFile = path.join(path.dirname(indexFile), newBase);
if (!fs.existsSync(newFile)) {
fs.copyFileSync(indexFile, newFile);
}
return { oldBase, newBase, newFile };
}
function patchAppShellImporters(assetFiles, oldBase, newBase) {
const touched = [];
for (const file of assetFiles) {
if (path.basename(file) === oldBase) continue;
let source = read(file);
let next = source
.replace(new RegExp(`\\./${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `./${newBase}`)
.replace(new RegExp(`/assets/${escapeRegex(oldBase)}(?:\\?v=rr-app-shell-[A-Za-z0-9-]+)?`, "g"), `/assets/${newBase}`);
if (next !== source) {
write(file, next);
touched.push(file);
}
}
return touched;
}
function patchPublicPdf(source) { function patchPublicPdf(source) {
if (!source.includes("rr-browser-buffer-polyfill")) { if (!source.includes("rr-browser-buffer-polyfill")) {
const importPrelude = source.match(/^(?:import[^;]+;)+/); const importPrelude = source.match(/^(?:import[^;]+;)+/);
@@ -241,6 +342,16 @@ if (ssrFile) {
warn("SSR bundle with generateFilename not found"); warn("SSR bundle with generateFilename not found");
} }
const appShell = patchAppShellEntry(assetFiles);
const appShellTouchedImporters = appShell ? patchAppShellImporters(assetFiles, appShell.oldBase, appShell.newBase) : [];
if (appShell) {
const appManifestFiles = [
...listFilesRecursive(path.join(outputDir, "server"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
...listFilesRecursive(path.join(appDir, "apps/server/dist"), (file) => path.basename(file).startsWith("_tanstack-start-manifest") && file.endsWith(".mjs")),
];
for (const file of appManifestFiles) patchAppManifest(file, appShell.oldBase, appShell.newBase);
}
const pdfFile = assetFiles const pdfFile = assetFiles
.filter((file) => path.basename(file).startsWith("pdf-document-")) .filter((file) => path.basename(file).startsWith("pdf-document-"))
.sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || ""; .sort((a, b) => fs.statSync(b).size - fs.statSync(a).size)[0] || "";
@@ -270,9 +381,24 @@ for (const file of assetFiles) {
if (fs.existsSync(serverIndexFile)) { if (fs.existsSync(serverIndexFile)) {
let serverIndex = read(serverIndexFile); let serverIndex = read(serverIndexFile);
for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters].filter(Boolean)) { for (const file of [...patchedFilenameFiles, pdfFile, ...touchedImporters, ...appShellTouchedImporters].filter(Boolean)) {
serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file); serverIndex = patchStaticManifestEntry(serverIndex, `/assets/${path.basename(file)}`, file);
} }
if (appShell) {
serverIndex = cloneStaticManifestEntry(
serverIndex,
`/assets/${appShell.oldBase}`,
`/assets/${appShell.newBase}`,
appShell.oldBase,
appShell.newBase,
appShell.newFile,
);
}
const serviceWorkerFile = path.join(path.dirname(assetsDir), "sw.js");
if (fs.existsSync(serviceWorkerFile)) {
write(serviceWorkerFile, serviceWorkerCleanup);
serverIndex = patchStaticManifestEntry(serverIndex, "/sw.js", serviceWorkerFile);
}
write(serverIndexFile, serverIndex); write(serverIndexFile, serverIndex);
} }

View File

@@ -111,6 +111,130 @@ if docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-
fi fi
docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R "String(t).trim().replace" "$APP_DIR/.output/public/assets"/file-*.js >/dev/null 2>&1' \ docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-resume-app-dir); grep -R "String(t).trim().replace" "$APP_DIR/.output/public/assets"/file-*.js >/dev/null 2>&1' \
|| fail "direct 包未在 file-*.js 下载工具中应用文件名补丁" || fail "direct 包未在 file-*.js 下载工具中应用文件名补丁"
curl -fsS "http://127.0.0.1:3004/" >/tmp/reactive-resume-home.html \
|| fail "direct 包首页无法访问"
grep -E '/assets/index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' /tmp/reactive-resume-home.html >/dev/null \
|| fail "direct 包首页未改用防缓存的 index 主入口文件名"
curl -fsS "http://127.0.0.1:3004/sw.js" >/tmp/reactive-resume-sw.js \
|| fail "direct 包 sw.js 无法访问"
grep -q 'disable stale PWA caches' /tmp/reactive-resume-sw.js \
|| fail "direct 包 sw.js 未替换为清理旧缓存版本"
if command -v google-chrome >/dev/null 2>&1 && command -v node >/dev/null 2>&1; then
log "使用 Chrome 执行 direct 包首页 JS"
node <<'NODE'
const { spawn } = require("child_process");
const fs = require("fs");
const http = require("http");
const port = 9444;
const userData = "/tmp/reactive-resume-chrome-smoke";
fs.rmSync(userData, { recursive: true, force: true });
const chrome = spawn("google-chrome", [
"--headless=new",
"--disable-gpu",
"--no-sandbox",
`--remote-debugging-port=${port}`,
`--user-data-dir=${userData}`,
"about:blank",
], { stdio: ["ignore", "ignore", "ignore"] });
function request(path, method = "GET") {
return new Promise((resolve, reject) => {
const req = http.request({ host: "127.0.0.1", port, path, method }, (res) => {
let body = "";
res.on("data", (chunk) => body += chunk);
res.on("end", () => resolve({ status: res.statusCode, body }));
});
req.on("error", reject);
req.end();
});
}
async function waitForChrome() {
for (let i = 0; i < 60; i++) {
try {
const res = await request("/json/version");
if (res.status === 200) return;
} catch {}
await new Promise((resolve) => setTimeout(resolve, 100));
}
throw new Error("Chrome remote debugging was not ready");
}
(async () => {
await waitForChrome();
const tab = JSON.parse((await request("/json/new?about:blank", "PUT")).body);
const ws = new WebSocket(tab.webSocketDebuggerUrl);
let id = 0;
const pending = new Map();
const errors = [];
const indexRequests = [];
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.id && pending.has(message.id)) {
pending.get(message.id)(message);
pending.delete(message.id);
return;
}
if (message.method === "Runtime.exceptionThrown") {
errors.push(message.params.exceptionDetails);
}
if (message.method === "Network.requestWillBeSent" && message.params.request.url.includes("index-")) {
indexRequests.push(message.params.request.url);
}
};
await new Promise((resolve) => { ws.onopen = resolve; });
function send(method, params = {}) {
return new Promise((resolve) => {
pending.set(++id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
}
await send("Runtime.enable");
await send("Page.enable");
await send("Network.enable");
await send("Page.navigate", { url: "http://127.0.0.1:3004/" });
await new Promise((resolve) => setTimeout(resolve, 5000));
const result = await send("Runtime.evaluate", {
expression: "document.body.innerText.includes('A free and open-source resume builder')",
returnByValue: true,
});
ws.close();
chrome.kill("SIGKILL");
if (errors.length > 0) {
console.error(JSON.stringify(errors.map((error) => ({
text: error.text,
url: error.url,
line: error.lineNumber,
column: error.columnNumber,
exception: error.exception?.description,
})), null, 2));
process.exit(1);
}
if (!result.result?.result?.value) {
console.error("Chrome did not render the expected home page text");
process.exit(1);
}
if (indexRequests.some((url) => /\/assets\/index-[A-Za-z0-9_-]+\.js$/.test(url) && !/-rr[0-9a-z]+\.js$/.test(url))) {
console.error(`Chrome still requested stale app shell: ${indexRequests.join(", ")}`);
process.exit(1);
}
})().catch((error) => {
chrome.kill("SIGKILL");
console.error(error);
process.exit(1);
});
NODE
else
log "未找到 google-chrome跳过浏览器级首页 JS 测试"
fi
log "离线检查 arm64/QNAP 镜像布局" log "离线检查 arm64/QNAP 镜像布局"
ARM64_DIGEST="$( ARM64_DIGEST="$(
@@ -173,6 +297,11 @@ grep -R -F 'replace(/[\\/:*?"<>|]/g' "$ARM64_ASSETS_DIR" >/dev/null \
if grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$ARM64_ASSETS_DIR" >/dev/null 2>&1; then if grep -R -E "index-[A-Za-z0-9_-]+\\.js\\?v=rr-filename-title" "$ARM64_ASSETS_DIR" >/dev/null 2>&1; then
fail "arm64 补丁错误地给 index 主入口追加了 rr-filename-title 缓存标记" fail "arm64 补丁错误地给 index 主入口追加了 rr-filename-title 缓存标记"
fi fi
find "$TMP_DIR/arm64-root/app" -type f -exec grep -qE 'index-[A-Za-z0-9_-]+-rr[0-9a-z]+\.js' {} \; -print -quit \
| grep -q . \
|| fail "arm64 补丁未改用防缓存的 index 主入口文件名"
find "$TMP_DIR/arm64-root/app" -path '*/sw.js' -type f -print0 | xargs -0 grep -l 'disable stale PWA caches' >/dev/null \
|| fail "arm64 sw.js 未替换为清理旧缓存版本"
grep -q 'function generateFilename(prefix, extension)' "$ARM64_FILENAME_ENTRY" \ grep -q 'function generateFilename(prefix, extension)' "$ARM64_FILENAME_ENTRY" \
|| fail "arm64 server entry 未包含 generateFilename" || fail "arm64 server entry 未包含 generateFilename"
grep -F 'filename.replace(/[\\/:*?"<>|]/g' "$ARM64_FILENAME_ENTRY" >/dev/null \ grep -F 'filename.replace(/[\\/:*?"<>|]/g' "$ARM64_FILENAME_ENTRY" >/dev/null \