Run development on the dev server's code, not a build's cache
A production deploy leaves its service worker on the device, and it claims every tab. It serves /.bunext/public/* cache first without knowing which build asked, while every cache-busting mechanism the app has (the precache gate, /build-info.json, the loading page) is production only. So switching to `bun run dev` kept serving the old build, vendor files included, and wrote the dev files back into the production build's cache. Development now clears that worker and every cache it wrote (`clear-development-service-worker.ts`, from ServiceWorkerRegistrar, which did nothing in development before), reloads once into the uncontrolled tab, and /sw.js answers with a stub that unregisters itself so Web Push and the loading page can't register it back.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Development must run on the dev server's code, never on what an
|
||||
* earlier production build left on the device. The worker registered by
|
||||
* a production deploy keeps controlling this origin (`clients.claim`),
|
||||
* and it serves every `/.bunext/public/*` file cache first without
|
||||
* knowing which build asked, while every cache-busting mechanism the app
|
||||
* has (the precache gate, `/build-info.json`, the loading page) is
|
||||
* production only. So in development the worker is unregistered and
|
||||
* every cache it wrote is dropped once, then the page reloads into an
|
||||
* uncontrolled tab.
|
||||
*
|
||||
* Returns whether anything was cleared, so the caller only reloads when
|
||||
* there was something to clear.
|
||||
*/
|
||||
export default async function clearDevelopmentServiceWorker(): Promise<boolean> {
|
||||
if (typeof navigator == "undefined") return false;
|
||||
if (!("serviceWorker" in navigator) || !window.isSecureContext) return false;
|
||||
|
||||
let cleared = false;
|
||||
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
|
||||
await Promise.all(
|
||||
registrations.map(async (registration) => {
|
||||
cleared = true;
|
||||
|
||||
/**
|
||||
* The worker's own caches go first, while it still owns
|
||||
* them, so nothing writes a build back mid-clear.
|
||||
*/
|
||||
await registration.unregister().catch(() => undefined);
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("caches" in window) {
|
||||
try {
|
||||
const keys = await caches.keys();
|
||||
|
||||
if (keys.length > 0) cleared = true;
|
||||
|
||||
await Promise.all(
|
||||
keys.map((key) => caches.delete(key).catch(() => false)),
|
||||
);
|
||||
} catch (error) {
|
||||
// A device with caches disabled still runs the dev server.
|
||||
}
|
||||
}
|
||||
|
||||
return cleared;
|
||||
}
|
||||
@@ -8,6 +8,36 @@ const PwaFiles: Record<string, string> = {
|
||||
[PwaManifestPath]: "manifest.webmanifest",
|
||||
};
|
||||
|
||||
/**
|
||||
* Development serves a worker that takes itself out instead of `sw.js`.
|
||||
* A production deploy leaves a worker on the device that keeps serving
|
||||
* its build's code cache first, and development never precaches or
|
||||
* updates anything, so a page opened from the dev server would go on
|
||||
* running the old build. The page clears it too
|
||||
* (`clear-development-service-worker.ts`); this catches every other
|
||||
* registration path (Web Push, the loading page, an old background tab).
|
||||
*/
|
||||
const DevelopmentServiceWorker = `self.addEventListener("install", () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
await self.registration.unregister().catch(() => undefined);
|
||||
if ("caches" in self) {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((key) => caches.delete(key)));
|
||||
}
|
||||
const clients = await self.clients.matchAll({ type: "window" });
|
||||
for (const client of clients) {
|
||||
if ("navigate" in client) await client.navigate(client.url).catch(() => undefined);
|
||||
}
|
||||
})(),
|
||||
);
|
||||
});
|
||||
`;
|
||||
|
||||
/**
|
||||
* Serves the service worker and the manifest with headers bunext's public
|
||||
* handler can't give them: neither may be cached by a browser or a CDN,
|
||||
@@ -23,9 +53,14 @@ export default async function handlePwaFile({
|
||||
|
||||
if (!file_name) return null;
|
||||
|
||||
const development_stub =
|
||||
file_name == "sw.js" && process.env.NODE_ENV != "production"
|
||||
? DevelopmentServiceWorker
|
||||
: null;
|
||||
|
||||
const file = Bun.file(path.join(PUBLIC_DIR, file_name));
|
||||
|
||||
if (!(await file.exists())) {
|
||||
if (!development_stub && !(await file.exists())) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
}
|
||||
|
||||
@@ -42,7 +77,7 @@ export default async function handlePwaFile({
|
||||
headers.set("Service-Worker-Allowed", "/");
|
||||
}
|
||||
|
||||
const response = new Response(file, { headers });
|
||||
const response = new Response(development_stub || file, { headers });
|
||||
|
||||
/**
|
||||
* `Bun.file` adds a filename disposition; a manifest is inline data.
|
||||
|
||||
Reference in New Issue
Block a user