From 2c3378b15f789cfef3bffd66d547b1c2751470a6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 11:46:21 +0000 Subject: [PATCH 1/4] test(web): reproduce #26437 boot error instead of reload on main chunk preload failure These tests reproduce #26437: when the main chunk's CSS or JS fails to load, bootstrap shows the boot error instead of reloading once, because the only vite:preloadError listener lives in the module that failed to load. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/bootstrap.test.ts | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 apps/web/src/bootstrap.test.ts diff --git a/apps/web/src/bootstrap.test.ts b/apps/web/src/bootstrap.test.ts new file mode 100644 index 0000000000000..7f5400006aa10 --- /dev/null +++ b/apps/web/src/bootstrap.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it, vi } from "vite-plus/test"; + +const mocks = vi.hoisted(() => ({ showBootError: vi.fn().mockName("showBootError") })); + +vi.mock("./lib/bootError", () => ({ showBootError: mocks.showBootError })); + +// Mirrors Vite's build preload helper when main's CSS or JS chunk fails to load. +vi.mock("./main", () => { + const error = new TypeError("Failed to fetch dynamically imported module: /assets/main.js"); + const event = Object.assign(new Event("vite:preloadError", { cancelable: true }), { + payload: error, + }); + window.dispatchEvent(event); + throw error; +}); + +function createStorageStub(initial: Record = {}): Storage { + const store = new Map(Object.entries(initial)); + return { + getItem: (key) => store.get(key) ?? null, + setItem: (key, value) => { + store.set(key, value); + }, + removeItem: (key) => { + store.delete(key); + }, + clear: () => store.clear(), + key: (index) => [...store.keys()][index] ?? null, + get length() { + return store.size; + }, + }; +} + +async function bootWithFailingMainChunk(storage: Storage) { + vi.resetModules(); + mocks.showBootError.mockClear(); + const { promise: settled, resolve } = Promise.withResolvers(); + const reload = vi.fn(() => resolve()).mockName("location.reload"); + mocks.showBootError.mockImplementation(() => resolve()); + vi.stubGlobal( + "window", + Object.assign(new EventTarget(), { sessionStorage: storage, location: { reload } }), + ); + try { + await import("./bootstrap"); + await settled; + return { reload, showBootError: mocks.showBootError }; + } finally { + vi.unstubAllGlobals(); + } +} + +describe("bootstrap", () => { + it("reloads once instead of showing the boot error when the main chunk fails to load", async () => { + const { reload, showBootError } = await bootWithFailingMainChunk(createStorageStub()); + + expect(reload).toHaveBeenCalledTimes(1); + expect(showBootError).not.toHaveBeenCalled(); + }); + + it("shows the boot error when the main chunk fails again after the reload", async () => { + const { reload, showBootError } = await bootWithFailingMainChunk( + createStorageStub({ "t3code:chunk-load-reloaded": "1" }), + ); + + expect(reload).not.toHaveBeenCalled(); + expect(showBootError).toHaveBeenCalledTimes(1); + }); +}); From fa5b574cef2ca6ba60f7dfbd34dda5b96de1160a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 11:49:00 +0000 Subject: [PATCH 2/4] fix(web): a failed main chunk load reloads once instead of showing the boot error The only vite:preloadError listener lived in main.tsx, the module whose load failed, so a stale main chunk went straight to the boot error. The listener now lives in chunkReloadGuard.ts and is registered by bootstrap.ts before importing main; main.tsx reads the shared chunkLoadState, so each event is decided once. Fixes pingdotgg/t3code#26437 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/bootstrap.test.ts | 5 ++++- apps/web/src/bootstrap.ts | 9 ++++++++- apps/web/src/lib/chunkReloadGuard.ts | 13 +++++++++++++ apps/web/src/main.tsx | 18 ++++-------------- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/apps/web/src/bootstrap.test.ts b/apps/web/src/bootstrap.test.ts index 7f5400006aa10..00057991784a9 100644 --- a/apps/web/src/bootstrap.test.ts +++ b/apps/web/src/bootstrap.test.ts @@ -35,7 +35,10 @@ function createStorageStub(initial: Record = {}): Storage { async function bootWithFailingMainChunk(storage: Storage) { vi.resetModules(); mocks.showBootError.mockClear(); - const { promise: settled, resolve } = Promise.withResolvers(); + let resolve!: () => void; + const settled = new Promise((done) => { + resolve = done; + }); const reload = vi.fn(() => resolve()).mockName("location.reload"); mocks.showBootError.mockImplementation(() => resolve()); vi.stubGlobal( diff --git a/apps/web/src/bootstrap.ts b/apps/web/src/bootstrap.ts index b6f422c5d68f4..3e2a37aefad96 100644 --- a/apps/web/src/bootstrap.ts +++ b/apps/web/src/bootstrap.ts @@ -1,4 +1,11 @@ import { showBootError } from "./lib/bootError"; +import { chunkLoadState, handleChunkLoadErrors } from "./lib/chunkReloadGuard"; + +handleChunkLoadErrors(); // Shared dev chunks need the React refresh preamble first. Catch failures before React mounts. -void import("./main").then(({ startup }) => startup).catch(showBootError); +void import("./main") + .then(({ startup }) => startup) + .catch((error: unknown) => { + if (!chunkLoadState.reloadScheduled) showBootError(error); + }); diff --git a/apps/web/src/lib/chunkReloadGuard.ts b/apps/web/src/lib/chunkReloadGuard.ts index c83e0e71e0f4c..ad226e322fd16 100644 --- a/apps/web/src/lib/chunkReloadGuard.ts +++ b/apps/web/src/lib/chunkReloadGuard.ts @@ -24,3 +24,16 @@ export function clearChunkReloadGuard(getStorage: () => Storage = () => window.s getStorage().removeItem(CHUNK_RELOAD_GUARD_KEY); } catch {} } + +export const chunkLoadState = { failed: false, reloadScheduled: false }; + +// Registered from the entry before main loads, so main-chunk and route-chunk failures share one decision. +export function handleChunkLoadErrors() { + window.addEventListener("vite:preloadError", (event) => { + chunkLoadState.failed = true; + if (reloadOnceForChunkLoadError()) { + chunkLoadState.reloadScheduled = true; + event.preventDefault(); + } + }); +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index d6cf4ce77b91b..d483e9cfae84f 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -8,22 +8,12 @@ import { hasCloudPublicConfig } from "./cloud/publicConfig"; import { isElectron } from "./env"; import { startAppServices } from "./appInit"; import { AppRoot } from "./AppRoot"; -import { clearChunkReloadGuard, reloadOnceForChunkLoadError } from "./lib/chunkReloadGuard"; +import { chunkLoadState, clearChunkReloadGuard } from "./lib/chunkReloadGuard"; const router = startAppServices(); const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string | undefined; -let chunkLoadFailed = false; -let reloadScheduled = false; -window.addEventListener("vite:preloadError", (event) => { - chunkLoadFailed = true; - if (reloadOnceForChunkLoadError()) { - reloadScheduled = true; - event.preventDefault(); - } -}); - const app = ; // Split by runtime to keep Clerk out of local startup and clerk-js out of browser builds. @@ -40,8 +30,8 @@ export const startup = Promise.all([ ]) .then(([ManagedAuthShell]) => { // Route chunk failures resolve router.load() and surface later through the error boundary. - if (reloadScheduled) return; - if (!chunkLoadFailed) clearChunkReloadGuard(); + if (chunkLoadState.reloadScheduled) return; + if (!chunkLoadState.failed) clearChunkReloadGuard(); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( {ManagedAuthShell && clerkPublishableKey ? ( @@ -53,6 +43,6 @@ export const startup = Promise.all([ ); }) .catch((error: unknown) => { - if (reloadScheduled) return; + if (chunkLoadState.reloadScheduled) return; throw error; }); From 89fa25d43b12d248e828e399eb918d3fdf85ee67 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 28 Oct 2026 11:58:04 +0000 Subject: [PATCH 3/4] test(web): await bootstrap's boot chain before asserting no boot error Co-Authored-By: Claude Opus 5.5 --- apps/web/src/bootstrap.test.ts | 11 +++-------- apps/web/src/bootstrap.ts | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/web/src/bootstrap.test.ts b/apps/web/src/bootstrap.test.ts index 00057991784a9..d2909d28e5c41 100644 --- a/apps/web/src/bootstrap.test.ts +++ b/apps/web/src/bootstrap.test.ts @@ -35,19 +35,14 @@ function createStorageStub(initial: Record = {}): Storage { async function bootWithFailingMainChunk(storage: Storage) { vi.resetModules(); mocks.showBootError.mockClear(); - let resolve!: () => void; - const settled = new Promise((done) => { - resolve = done; - }); - const reload = vi.fn(() => resolve()).mockName("location.reload"); - mocks.showBootError.mockImplementation(() => resolve()); + const reload = vi.fn().mockName("location.reload"); vi.stubGlobal( "window", Object.assign(new EventTarget(), { sessionStorage: storage, location: { reload } }), ); try { - await import("./bootstrap"); - await settled; + const { boot } = await import("./bootstrap"); + await boot; return { reload, showBootError: mocks.showBootError }; } finally { vi.unstubAllGlobals(); diff --git a/apps/web/src/bootstrap.ts b/apps/web/src/bootstrap.ts index 3e2a37aefad96..fe07e58b02872 100644 --- a/apps/web/src/bootstrap.ts +++ b/apps/web/src/bootstrap.ts @@ -4,7 +4,7 @@ import { chunkLoadState, handleChunkLoadErrors } from "./lib/chunkReloadGuard"; handleChunkLoadErrors(); // Shared dev chunks need the React refresh preamble first. Catch failures before React mounts. -void import("./main") +export const boot = import("./main") .then(({ startup }) => startup) .catch((error: unknown) => { if (!chunkLoadState.reloadScheduled) showBootError(error); From 246665a5a433c3b93e534e68350b41a26f763bb0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 30 Oct 2026 07:23:21 +0000 Subject: [PATCH 4/4] test(web): bundled dev fixture copies the chunk reload guard bootstrap now imports Co-Authored-By: Claude Opus 5.5 --- apps/web/src/bundledDev.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/src/bundledDev.test.ts b/apps/web/src/bundledDev.test.ts index 770b26e0a686b..5517910d754db 100644 --- a/apps/web/src/bundledDev.test.ts +++ b/apps/web/src/bundledDev.test.ts @@ -26,7 +26,12 @@ it("initializes React refresh before a shared UI chunk runs in bundled dev", asy try { await NodeFSP.mkdir(NodePath.join(root, "src/lib"), { recursive: true }); await NodeFSP.writeFile(NodePath.join(root, "package.json"), '{"type":"module"}'); - for (const file of ["index.html", "src/bootstrap.ts", "src/lib/bootError.ts"]) { + for (const file of [ + "index.html", + "src/bootstrap.ts", + "src/lib/bootError.ts", + "src/lib/chunkReloadGuard.ts", + ]) { await NodeFSP.copyFile(new URL(`../${file}`, import.meta.url), NodePath.join(root, file)); } await NodeFSP.writeFile( @@ -94,6 +99,7 @@ export const startup = Promise.resolve().then(() => globalThis.onStarted(Shared( `import assert from "node:assert/strict"; const started = Promise.withResolvers(); globalThis.window = globalThis; +globalThis.addEventListener = () => {}; globalThis.document = { createElement: () => ({ relList: { supports: () => true } }), getElementById: () => null,