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

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 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);
}

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 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);
}