fix stale frontend cache in install packages
This commit is contained in:
6
dist/SHA256SUMS
vendored
6
dist/SHA256SUMS
vendored
@@ -1,3 +1,3 @@
|
||||
4310b841d283d76465e8f7c53d630b24ad10e75ae5b6039399db1adcded8702e reactive-resume-clean-install-20260520.zip
|
||||
c2a24e404fe47bf4c21c01d22fa21af0c86323ef246606fe7152ad293b46ab34 reactive-resume-personal-direct-20260520.zip
|
||||
5d26c24423159df199b9c2263d02de64912d7c4f44f7e17bf5beef2d2d601e8f reactive-resume-personal-qnap-nas-20260520.zip
|
||||
1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
|
||||
34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
|
||||
933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip
|
||||
|
||||
6
dist/SHA256SUMS-20260520
vendored
6
dist/SHA256SUMS-20260520
vendored
@@ -1,3 +1,3 @@
|
||||
4310b841d283d76465e8f7c53d630b24ad10e75ae5b6039399db1adcded8702e reactive-resume-clean-install-20260520.zip
|
||||
c2a24e404fe47bf4c21c01d22fa21af0c86323ef246606fe7152ad293b46ab34 reactive-resume-personal-direct-20260520.zip
|
||||
5d26c24423159df199b9c2263d02de64912d7c4f44f7e17bf5beef2d2d601e8f reactive-resume-personal-qnap-nas-20260520.zip
|
||||
1a1571bbed2c59c0003daa3c4c41cda7464b03cb8c4706bd7bf507e8abfaa7ea reactive-resume-clean-install-20260520.zip
|
||||
34764a874e5477e439fa2860ef311e42264e90575c5a89e3f1651748708ef4df reactive-resume-personal-direct-20260520.zip
|
||||
933541058567f73b6a0d9521863a0d592da6d0a546fd1c38587aeaa0f11cf61d reactive-resume-personal-qnap-nas-20260520.zip
|
||||
|
||||
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
BIN
dist/reactive-resume-clean-install-20260520.zip
vendored
Binary file not shown.
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
BIN
dist/reactive-resume-personal-direct-20260520.zip
vendored
Binary file not shown.
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
BIN
dist/reactive-resume-personal-qnap-nas-20260520.zip
vendored
Binary file not shown.
@@ -62,7 +62,21 @@ const explicitSsrFile = process.env.SSR_FILE || "";
|
||||
const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs");
|
||||
const filenameCacheBust = "rr-filename-title-20260520b";
|
||||
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 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) {
|
||||
console.warn(`Reactive Resume runtime patch: ${message}`);
|
||||
@@ -111,6 +125,20 @@ function listJsFiles(dir) {
|
||||
.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) {
|
||||
return source.includes(to) ? source : source.replace(from, to);
|
||||
}
|
||||
@@ -185,6 +213,79 @@ function patchSsr(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) {
|
||||
if (!source.includes("rr-browser-buffer-polyfill")) {
|
||||
const importPrelude = source.match(/^(?:import[^;]+;)+/);
|
||||
@@ -241,6 +342,16 @@ if (ssrFile) {
|
||||
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
|
||||
.filter((file) => path.basename(file).startsWith("pdf-document-"))
|
||||
.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)) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,21 @@ const explicitSsrFile = process.env.SSR_FILE || "";
|
||||
const serverIndexFile = process.env.SERVER_INDEX_FILE || path.join(outputDir, "server/index.mjs");
|
||||
const filenameCacheBust = "rr-filename-title-20260520b";
|
||||
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 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) {
|
||||
console.warn(`Reactive Resume runtime patch: ${message}`);
|
||||
@@ -111,6 +125,20 @@ function listJsFiles(dir) {
|
||||
.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) {
|
||||
return source.includes(to) ? source : source.replace(from, to);
|
||||
}
|
||||
@@ -185,6 +213,79 @@ function patchSsr(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) {
|
||||
if (!source.includes("rr-browser-buffer-polyfill")) {
|
||||
const importPrelude = source.match(/^(?:import[^;]+;)+/);
|
||||
@@ -241,6 +342,16 @@ if (ssrFile) {
|
||||
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
|
||||
.filter((file) => path.basename(file).startsWith("pdf-document-"))
|
||||
.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)) {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,130 @@ if docker exec "$PROJECT-reactive-resume-1" sh -lc 'APP_DIR=$(cat /tmp/reactive-
|
||||
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' \
|
||||
|| 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 镜像布局"
|
||||
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
|
||||
fail "arm64 补丁错误地给 index 主入口追加了 rr-filename-title 缓存标记"
|
||||
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" \
|
||||
|| fail "arm64 server entry 未包含 generateFilename"
|
||||
grep -F 'filename.replace(/[\\/:*?"<>|]/g' "$ARM64_FILENAME_ENTRY" >/dev/null \
|
||||
|
||||
Reference in New Issue
Block a user