Download the whole app build behind a loading page
In production, a page request without the running build's cookie gets a small loading page instead of the app. It downloads every file in .bunext/public (listed at /build-info.json) into that build's cache, checks they are all there, hands over to a new service worker waiting behind an older one, sets the cookie and reloads. The service worker then serves the app's code from the cache, so later visits load it from the device. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -3,9 +3,10 @@
|
||||
* (`chunk-AEHOZS24.js`, `admin/bots/H6VDJMBN.js`, `X4TQCLSA.css`): one
|
||||
* at such a path never changes, so it can be cached for good. The
|
||||
* vendor files (`/.bunext/public/vendor/react.js`) keep fixed names and
|
||||
* are not. `public/sw.js` repeats this pattern.
|
||||
* are not. The loading page (`public/loading.html`) is generated with
|
||||
* this pattern.
|
||||
*/
|
||||
const HashedAssetPattern = /^\/\.bunext\/public\/pages\/(?:.*\/)?(?:chunk-)?[A-Z0-9]{8}\.(?:js|css)$/;
|
||||
export const HashedAssetPattern = /^\/\.bunext\/public\/pages\/(?:.*\/)?(?:chunk-)?[A-Z0-9]{8}\.(?:js|css)$/;
|
||||
|
||||
type Params = {
|
||||
pathname: string;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import needsPrecachePage from "./needs-precache-page";
|
||||
|
||||
const page = {
|
||||
method: "GET",
|
||||
pathname: "/admin/bots/1",
|
||||
fetch_dest: "document",
|
||||
build_id: "build-2",
|
||||
};
|
||||
|
||||
describe("needsPrecachePage", () => {
|
||||
test("stands in for a page on a build the browser hasn't downloaded", () => {
|
||||
expect(needsPrecachePage(page)).toBe(true);
|
||||
expect(needsPrecachePage({ ...page, cookie_build_id: "build-1" })).toBe(true);
|
||||
expect(
|
||||
needsPrecachePage({ ...page, fetch_dest: "empty", fetch_mode: "navigate" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
needsPrecachePage({ ...page, fetch_dest: null, accept: "text/html,*/*" }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("lets the current build through", () => {
|
||||
expect(needsPrecachePage({ ...page, cookie_build_id: "build-2" })).toBe(false);
|
||||
});
|
||||
|
||||
test("never runs in development", () => {
|
||||
expect(needsPrecachePage({ ...page, build_id: null })).toBe(false);
|
||||
});
|
||||
|
||||
test("leaves everything that isn't a page navigation", () => {
|
||||
expect(needsPrecachePage({ ...page, method: "POST" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, fetch_dest: "script" })).toBe(false);
|
||||
expect(
|
||||
needsPrecachePage({
|
||||
...page,
|
||||
fetch_dest: "empty",
|
||||
fetch_mode: "cors",
|
||||
accept: "text/html",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, fetch_dest: null, accept: "*/*" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/api/admin/bots/send-message" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/.bunext/public/vendor/react.js" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/computers/2/vnc/vnc.html" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/media/1/private/a" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/build-info.json" })).toBe(false);
|
||||
expect(needsPrecachePage({ ...page, pathname: "/sw.js" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Paths the loading page never stands in for: the app's code, live data,
|
||||
* media, proxied computers and the files the loading page itself needs.
|
||||
*/
|
||||
const OpenPrefixes = ["/.bunext/", "/api/", "/ws", "/media", "/computers/"];
|
||||
|
||||
type Params = {
|
||||
method: string;
|
||||
pathname: string;
|
||||
/**
|
||||
* `Sec-Fetch-Mode` and `Sec-Fetch-Dest`, when the browser sends them.
|
||||
* A navigation the service worker passes on keeps `navigate` but
|
||||
* arrives with the destination `empty`.
|
||||
*/
|
||||
fetch_mode?: string | null;
|
||||
fetch_dest?: string | null;
|
||||
accept?: string | null;
|
||||
/**
|
||||
* The build in the browser's cookie, if any.
|
||||
*/
|
||||
cookie_build_id?: string | null;
|
||||
/**
|
||||
* The running build; null in development.
|
||||
*/
|
||||
build_id?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a request is a browser opening a page of the app on a build it
|
||||
* hasn't downloaded yet, so it gets the loading page instead. Only page
|
||||
* navigations: anything with a file extension (`/favicon.ico`,
|
||||
* `/sw.js`, `/build-info.json`) goes through as usual.
|
||||
*/
|
||||
export default function needsPrecachePage({
|
||||
method,
|
||||
pathname,
|
||||
fetch_mode,
|
||||
fetch_dest,
|
||||
accept,
|
||||
cookie_build_id,
|
||||
build_id,
|
||||
}: Params): boolean {
|
||||
if (!build_id) return false;
|
||||
if (cookie_build_id == build_id) return false;
|
||||
if (method != "GET") return false;
|
||||
if (OpenPrefixes.some((prefix) => pathname.startsWith(prefix))) return false;
|
||||
if (pathname.split("/").pop()?.includes(".")) return false;
|
||||
|
||||
if (fetch_mode || fetch_dest) {
|
||||
return fetch_mode == "navigate" || fetch_dest == "document";
|
||||
}
|
||||
|
||||
return Boolean(accept?.includes("text/html"));
|
||||
}
|
||||
Reference in New Issue
Block a user