From 103eeeb79472cddeca0665deadc0c53e22f298e3 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Mon, 13 Jul 2026 09:59:54 +0100 Subject: [PATCH 1/2] fix(frontend): self-heal tabs running a stale build after deploys Tabs opened before a deploy keep running the old bundle; when they lazily load the wallet chunk the old hashed file is gone and the user sees raw module errors ('module factory is not available'). Minimal fix, no bundling changes: - isStaleChunk classifier for missing-chunk error shapes. - StaleDeployGuard: global error/unhandledrejection listeners reload once onto the current build (sessionStorage loop protection). - Wallet connect and signing catches show 'StackStream was updated, refreshing' and reload instead of surfacing module internals. --- frontend/src/app/layout.tsx | 2 + .../src/components/stale-deploy-guard.tsx | 55 +++++++++++++++++++ frontend/src/hooks/use-stacks-tx.ts | 14 ++++- frontend/src/lib/wallet-errors.ts | 11 ++++ frontend/src/providers/stacks-provider.tsx | 8 +++ 5 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/stale-deploy-guard.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 9a74d28..8069c0b 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,6 +3,7 @@ import { GeistSans } from "geist/font/sans"; import { GeistMono } from "geist/font/mono"; import { QueryProvider } from "@/providers/query-provider"; import { StacksProvider } from "@/providers/stacks-provider"; +import { StaleDeployGuard } from "@/components/stale-deploy-guard"; import { Toaster } from "sonner"; import "./globals.css"; @@ -28,6 +29,7 @@ export default function RootLayout({ className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-surface-0 text-zinc-100`} > + {children} { + const onError = (event: ErrorEvent) => { + if (isStaleChunk(event.error ?? event.message)) { + event.preventDefault(); + reloadOnceForNewDeploy(); + } + }; + const onRejection = (event: PromiseRejectionEvent) => { + if (isStaleChunk(event.reason)) { + event.preventDefault(); + reloadOnceForNewDeploy(); + } + }; + window.addEventListener("error", onError); + window.addEventListener("unhandledrejection", onRejection); + return () => { + window.removeEventListener("error", onError); + window.removeEventListener("unhandledrejection", onRejection); + }; + }, []); + + return null; +} diff --git a/frontend/src/hooks/use-stacks-tx.ts b/frontend/src/hooks/use-stacks-tx.ts index c66adc3..2eca699 100644 --- a/frontend/src/hooks/use-stacks-tx.ts +++ b/frontend/src/hooks/use-stacks-tx.ts @@ -5,7 +5,12 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { PostConditionMode } from "@stacks/transactions"; import { waitForTxConfirmation, clarityErrorMessage } from "@/lib/stacks"; -import { isUserCancel, isNoResponse, walletErrorDetail } from "@/lib/wallet-errors"; +import { + isUserCancel, + isNoResponse, + isStaleChunk, + walletErrorDetail, +} from "@/lib/wallet-errors"; type TxStatus = "idle" | "pending" | "confirming" | "success" | "error"; @@ -142,6 +147,13 @@ export function useStacksTx() { setStatus("idle"); return null; } + if (isStaleChunk(err)) { + // A deploy replaced the bundle files under this open tab — reload + // onto the current build instead of surfacing module internals. + toast.info("StackStream was updated — refreshing…"); + setTimeout(() => window.location.reload(), 800); + return null; + } setError(walletErrorDetail(err)); setStatus("error"); return null; diff --git a/frontend/src/lib/wallet-errors.ts b/frontend/src/lib/wallet-errors.ts index 4038792..310f030 100644 --- a/frontend/src/lib/wallet-errors.ts +++ b/frontend/src/lib/wallet-errors.ts @@ -45,3 +45,14 @@ export function walletErrorDetail(err: unknown): string { const msg = e.message?.slice(0, 140) ?? "Unknown error"; return e.code !== undefined ? `${msg} (code ${e.code})` : msg; } + +/** + * A lazily-loaded chunk vanished because a new deploy replaced the hashed + * bundle files while this tab was still running the old build. The only + * correct recovery is a one-time page reload onto the new build. + */ +export function isStaleChunk(err: unknown): boolean { + return /module factory|ChunkLoadError|Loading chunk|Failed to fetch dynamically imported module|Importing a module script failed/i.test( + asErrorLike(err).message ?? "" + ); +} diff --git a/frontend/src/providers/stacks-provider.tsx b/frontend/src/providers/stacks-provider.tsx index 3920bba..0ccf043 100644 --- a/frontend/src/providers/stacks-provider.tsx +++ b/frontend/src/providers/stacks-provider.tsx @@ -24,6 +24,7 @@ import { isUserCancel, isNoResponse, isNoWalletFound, + isStaleChunk, walletErrorDetail, } from "@/lib/wallet-errors"; @@ -142,6 +143,13 @@ export function useStacksAuth() { ); } catch (err) { console.error("[wallet connect]", err); + if (isStaleChunk(err)) { + // A deploy replaced the bundle files under this open tab — reload + // onto the current build instead of surfacing module internals. + toast.info("StackStream was updated — refreshing…"); + setTimeout(() => window.location.reload(), 800); + return; + } toast.error(`Wallet connection failed: ${walletErrorDetail(err)}`, { duration: 8000, }); From 063f7b5a61a6e8904ae5586f01eca7d86ccf5142 Mon Sep 17 00:00:00 2001 From: jayteemoney Date: Mon, 13 Jul 2026 10:47:54 +0100 Subject: [PATCH 2/2] =?UTF-8?q?fix(frontend):=20build=20with=20webpack=20?= =?UTF-8?q?=E2=80=94=20Turbopack=20breaks=20the=20wallet=20chunk=20graph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of every 'module factory is not available' failure since the v8 migration: Turbopack (Next 16.1.6) mis-wires @stacks/connect's lazily-imported module graph in production builds, so clicking Connect always failed in Turbopack-built bundles — on first load, any browser. It only ever worked in dev because dev bundling differs. Not a stale- cache problem; module ids were stable across builds because the defect is deterministic. Reproduced headlessly (puppeteer clicking Connect on a served production build) and verified fixed: with --webpack the wallet chooser opens with zero console errors. transpilePackages did not fix the Turbopack graph, so the build script switches bundlers; dev stays on Turbopack, where the package bundles fine. --- frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7e02152..33b82e8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, @@ -40,4 +40,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} +} \ No newline at end of file