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 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, });