Fetch the rest of a new build in the background (#394)

The app page names only what it loads at start. The composer, settings,
viewers and the rest were fetched when first used, and after every deploy
that first use waited on the server -- and the worker's tidy-up dropped
them again at the next deploy anyway.

The build now writes the list of all its files into the page as an inert
JSON block. The service worker keeps everything listed and, once a page
names files it does not hold, fetches them three at a time; a load cut
short is resumed at the next navigation. Language catalogs are listed
apart and left to be cached when used, and nothing is fetched ahead when
the browser is set to save data.
This commit is contained in:
jcoffey
2026-09-16 13:29:57 -07:00
committed by GitHub
parent e158ebac5a
commit 4c67460450
2 changed files with 86 additions and 6 deletions
+37 -2
View File
@@ -1,4 +1,4 @@
import { defineConfig } from "vitest/config";
import { defineConfig, type Plugin } from "vitest/config";
import react from "@vitejs/plugin-react";
import { fileURLToPath, URL } from "node:url";
import { resolveVersion } from "../scripts/version.mjs";
@@ -22,9 +22,44 @@ const version = resolveVersion();
*/
const base = baseUrlOf(process.env.BASE_PATH);
/*
* Every file the build made, written into the app page for the service worker.
*
* The page names only what it loads at start; the composer, settings, viewers
* and the rest arrive when first used, and after each deploy that first use
* went back to the server. With the whole list in the page, the worker can
* fetch them in the background once a new version is seen, and knows to keep
* them. Language catalogs are listed apart: a reader wants one of them, which
* is cached when it is first loaded.
*
* An inert JSON block rather than prefetch links, which the browser would
* fetch on every load.
*/
function assetList(): Plugin {
return {
name: "ihasmail-asset-list",
apply: "build",
transformIndexHtml: {
order: "post",
handler(html, ctx) {
if (!ctx.bundle) return html;
const precache: string[] = [];
const onDemand: string[] = [];
for (const file of Object.values(ctx.bundle)) {
if (!file.fileName.startsWith("assets/") || file.fileName.endsWith(".map")) continue;
const catalog = file.type === "chunk" && file.moduleIds.length > 0 && file.moduleIds.every((id) => /[\\/]src[\\/]locales[\\/][^\\/]+\.ts$/.test(id));
(catalog ? onDemand : precache).push(`${base}${file.fileName}`);
}
const json = JSON.stringify({ precache: precache.sort(), onDemand: onDemand.sort() });
return html.replace("</body>", ` <script type="application/json" id="ihasmail-assets">${json}</script>\n </body>`);
},
},
};
}
export default defineConfig({
base,
plugins: [react()],
plugins: [react(), assetList()],
define: { __IHASMAIL_VERSION__: JSON.stringify(version) },
resolve: {
alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) },