- Wrap the main application layout in an AppErrorBoundary to enhance error handling. - Refactor BOOTSTRAP_REQUIRED_DEPENDENCIES to use a structured format, allowing for optional install specifiers for better dependency management.
Unified · Split
| @@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/sonner"; · 4 unmodified lines | ||
| 5 | 5 | import { AppConvexProvider } from "@/lib/convex-provider"; |
| 6 | 6 | import { QueryProvider } from "@/lib/query-provider"; |
| 7 | 7 | import { SessionProvider } from "@/lib/session-context"; |
| 8 | +import { AppErrorBoundary } from "@/components/app-error-boundary"; | |
| 8 | 9 | import "./globals.css"; |
| 9 | 10 | |
| 10 | 11 | const inter = Inter({ |
| @@ -38,13 +39,15 @@ export default function RootLayout({ · 27 unmodified lines | ||
| 38 | 39 | enableSystem |
| 39 | 40 | enableColorScheme |
| 40 | 41 | > |
| 41 | − <QueryProvider> | |
| 42 | − <AppConvexProvider> | |
| 43 | − <SessionProvider> | |
| 44 | − {children} | |
| 45 | − </SessionProvider> | |
| 46 | − </AppConvexProvider> | |
| 47 | − </QueryProvider> | |
| 42 | + <AppErrorBoundary> | |
| 43 | + <QueryProvider> | |
| 44 | + <AppConvexProvider> | |
| 45 | + <SessionProvider> | |
| 46 | + {children} | |
| 47 | + </SessionProvider> | |
| 48 | + </AppConvexProvider> | |
| 49 | + </QueryProvider> | |
| 50 | + </AppErrorBoundary> | |
| 48 | 51 | <Toaster position="bottom-right" /> |
| 49 | 52 | </ThemeProvider> |
| 50 | 53 | </body> |
| @@ -0,0 +1,119 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { Component } from "react"; | |
| 4 | +import { AlertTriangle, RotateCcw } from "lucide-react"; | |
| 5 | +import type { ErrorInfo, ReactNode } from "react"; | |
| 6 | +import { Button } from "@/components/ui/button"; | |
| 7 | +import { clearAnonymousAuth } from "@/lib/anonymous-auth"; | |
| 8 | +import { clearSessionStorage } from "@/lib/session-storage"; | |
| 9 | + | |
| 10 | +type Props = { | |
| 11 | + children: ReactNode; | |
| 12 | +}; | |
| 13 | + | |
| 14 | +type State = { | |
| 15 | + error: Error | null; | |
| 16 | + componentErrorInfo: ErrorInfo | null; | |
| 17 | +}; | |
| 18 | + | |
| 19 | +const CLIENT_STORAGE_PREFIX = "executor-"; | |
| 20 | + | |
| 21 | +function clearClientStorage() { | |
| 22 | + if (typeof window === "undefined") { | |
| 23 | + return; | |
| 24 | + } | |
| 25 | + | |
| 26 | + clearSessionStorage(); | |
| 27 | + clearAnonymousAuth({ clearAccount: true }); | |
| 28 | + | |
| 29 | + const clearPrefixedStorage = (storage: Storage) => { | |
| 30 | + const keysToDelete: string[] = []; | |
| 31 | + | |
| 32 | + for (let index = storage.length - 1; index >= 0; index--) { | |
| 33 | + const key = storage.key(index); | |
| 34 | + if (key && key.startsWith(CLIENT_STORAGE_PREFIX)) { | |
| 35 | + keysToDelete.push(key); | |
| 36 | + } | |
| 37 | + } | |
| 38 | + | |
| 39 | + for (const key of keysToDelete) { | |
| 40 | + storage.removeItem(key); | |
| 41 | + } | |
| 42 | + }; | |
| 43 | + | |
| 44 | + clearPrefixedStorage(window.localStorage); | |
| 45 | + clearPrefixedStorage(window.sessionStorage); | |
| 46 | +} | |
| 47 | + | |
| 48 | +function buildErrorDetails(error: Error | null, errorInfo: ErrorInfo | null) { | |
| 49 | + const lines = [error?.message ?? "Unknown error"]; | |
| 50 | + | |
| 51 | + if (error?.stack) { | |
| 52 | + lines.push("", "Stack", error.stack); | |
| 53 | + } | |
| 54 | + | |
| 55 | + if (errorInfo?.componentStack) { | |
| 56 | + lines.push("", "Component stack", errorInfo.componentStack); | |
| 57 | + } | |
| 58 | + | |
| 59 | + return lines.join("\n"); | |
| 60 | +} | |
| 61 | + | |
| 62 | +export class AppErrorBoundary extends Component<Props, State> { | |
| 63 | + state: State = { | |
| 64 | + error: null, | |
| 65 | + componentErrorInfo: null, | |
| 66 | + }; | |
| 67 | + | |
| 68 | + static getDerivedStateFromError(error: Error) { | |
| 69 | + return { error, componentErrorInfo: null }; | |
| 70 | + } | |
| 71 | + | |
| 72 | + componentDidCatch(error: Error, errorInfo: ErrorInfo) { | |
| 73 | + this.setState({ error, componentErrorInfo: errorInfo }); | |
| 74 | + } | |
| 75 | + | |
| 76 | + handleRecover = () => { | |
| 77 | + try { | |
| 78 | + clearClientStorage(); | |
| 79 | + } finally { | |
| 80 | + window.location.assign("/sign-out"); | |
| 81 | + } | |
| 82 | + }; | |
| 83 | + | |
| 84 | + render() { | |
| 85 | + const { error, componentErrorInfo } = this.state; | |
| 86 | + | |
| 87 | + if (!error) { | |
| 88 | + return this.props.children; | |
| 89 | + } | |
| 90 | + | |
| 91 | + const details = buildErrorDetails(error, componentErrorInfo); | |
| 92 | + | |
| 93 | + return ( | |
| 94 | + <div className="min-h-screen grid place-items-center bg-background px-4 py-8"> | |
| 95 | + <div className="w-full max-w-2xl border border-border rounded-lg bg-card text-foreground p-6 space-y-4"> | |
| 96 | + <div className="flex items-start gap-3"> | |
| 97 | + <AlertTriangle className="h-5 w-5 text-destructive mt-0.5" /> | |
| 98 | + <div className="space-y-1 min-w-0"> | |
| 99 | + <h2 className="text-lg font-semibold">Something went wrong</h2> | |
| 100 | + <p className="text-sm text-muted-foreground">The app hit an unexpected error. Use refresh to clear cached state and sign out.</p> | |
| 101 | + </div> | |
| 102 | + </div> | |
| 103 | + | |
| 104 | + <details className="rounded border border-border/60 bg-background/60 p-3"> | |
| 105 | + <summary className="cursor-pointer text-sm font-medium">Show error details</summary> | |
| 106 | + <pre className="mt-3 max-h-72 overflow-auto rounded bg-black/80 text-[11px] leading-tight text-white/90 p-3 whitespace-pre-wrap break-all"> | |
| 107 | + {details} | |
| 108 | + </pre> | |
| 109 | + </details> | |
| 110 | + | |
| 111 | + <Button className="w-full md:w-auto" onClick={this.handleRecover}> | |
| 112 | + <RotateCcw className="h-4 w-4" /> | |
| 113 | + Refresh and recover | |
| 114 | + </Button> | |
| 115 | + </div> | |
| 116 | + </div> | |
| 117 | + ); | |
| 118 | + } | |
| 119 | +} | |
| @@ -16,20 +16,25 @@ const convexJsonSchema = z.object({ · 15 unmodified lines | ||
| 16 | 16 | functions: z.string().optional(), |
| 17 | 17 | }); |
| 18 | 18 | |
| 19 | −const BOOTSTRAP_REQUIRED_DEPENDENCIES = [ | |
| 20 | − "convex", | |
| 21 | − "@convex-dev/migrations", | |
| 22 | − "@convex-dev/stripe", | |
| 23 | − "@convex-dev/workos-authkit", | |
| 24 | − "@apidevtools/swagger-parser", | |
| 25 | − "@workos-inc/node", | |
| 26 | − "convex-helpers", | |
| 27 | − "jose", | |
| 28 | − "openapi-typescript", | |
| 29 | − "zod", | |
| 30 | − "graphql", | |
| 31 | − "better-result", | |
| 32 | − "@modelcontextprotocol/sdk", | |
| 19 | +type BootstrapDependency = { | |
| 20 | + packageName: string; | |
| 21 | + installSpecifier?: string; | |
| 22 | +}; | |
| 23 | + | |
| 24 | +const BOOTSTRAP_REQUIRED_DEPENDENCIES: BootstrapDependency[] = [ | |
| 25 | + { packageName: "convex" }, | |
| 26 | + { packageName: "@convex-dev/migrations" }, | |
| 27 | + { packageName: "@convex-dev/stripe" }, | |
| 28 | + { packageName: "@convex-dev/workos-authkit" }, | |
| 29 | + { packageName: "@apidevtools/swagger-parser" }, | |
| 30 | + { packageName: "@workos-inc/node" }, | |
| 31 | + { packageName: "convex-helpers" }, | |
| 32 | + { packageName: "jose" }, | |
| 33 | + { packageName: "openapi-typescript" }, | |
| 34 | + { packageName: "zod", installSpecifier: "zod@^3.25.0" }, | |
| 35 | + { packageName: "graphql" }, | |
| 36 | + { packageName: "better-result" }, | |
| 37 | + { packageName: "@modelcontextprotocol/sdk" }, | |
| 33 | 38 | ]; |
| 34 | 39 | |
| 35 | 40 | async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> { |
| @@ -157,7 +162,7 @@ async function hasBootstrapDependencies(projectDir: string): Promise<boolean> { · 121 unmodified lines | ||
| 157 | 162 | } |
| 158 | 163 | |
| 159 | 164 | for (const dependency of BOOTSTRAP_REQUIRED_DEPENDENCIES) { |
| 160 | − const dependencyPath = path.join(nodeModulesDir, ...dependency.split("/")); | |
| 165 | + const dependencyPath = path.join(nodeModulesDir, ...dependency.packageName.split("/")); | |
| 161 | 166 | if (!(await pathExists(dependencyPath))) { |
| 162 | 167 | return false; |
| 163 | 168 | } |
| @@ -198,7 +203,7 @@ async function ensureProjectDependencies(info: ManagedRuntimeInfo, projectDir: s · 34 unmodified lines | ||
| 198 | 203 | "--omit", |
| 199 | 204 | "dev", |
| 200 | 205 | "--ignore-scripts", |
| 201 | − ...BOOTSTRAP_REQUIRED_DEPENDENCIES, | |
| 206 | + ...BOOTSTRAP_REQUIRED_DEPENDENCIES.map((dependency) => dependency.installSpecifier ?? dependency.packageName), | |
| 202 | 207 | ], |
| 203 | 208 | { env, stdout: "pipe", stderr: "pipe" }, |
| 204 | 209 | ); |