History

Integrate AppErrorBoundary into RootLayout and refactor bootstrap dependencies structure

- 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.

Rhys Sullivan committed 9 months ago

Commit c458d211 parent 380105a3 files · +150 −23Browse files

Unified · Split

3 files
  1. executor/apps/web/src/app/layout.tsx +10 −7
  2. executor/apps/web/src/components/app-error-boundary.tsx +119 −0
  3. executor/packages/core/src/managed/runtime-bootstrap.ts +21 −16
executor/apps/web/src/app/layout.tsx+10−7
@@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/sonner"; · 4 unmodified lines
55 import { AppConvexProvider } from "@/lib/convex-provider";
66 import { QueryProvider } from "@/lib/query-provider";
77 import { SessionProvider } from "@/lib/session-context";
8+import { AppErrorBoundary } from "@/components/app-error-boundary";
89 import "./globals.css";
910
1011 const inter = Inter({
@@ -38,13 +39,15 @@ export default function RootLayout({ · 27 unmodified lines
3839 enableSystem
3940 enableColorScheme
4041 >
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>
4851 <Toaster position="bottom-right" />
4952 </ThemeProvider>
5053 </body>
executor/apps/web/src/components/app-error-boundary.tsx+119−0
@@ -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+}
executor/packages/core/src/managed/runtime-bootstrap.ts+21−16
@@ -16,20 +16,25 @@ const convexJsonSchema = z.object({ · 15 unmodified lines
1616 functions: z.string().optional(),
1717 });
1818
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" },
3338 ];
3439
3540 async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> {
@@ -157,7 +162,7 @@ async function hasBootstrapDependencies(projectDir: string): Promise<boolean> { · 121 unmodified lines
157162 }
158163
159164 for (const dependency of BOOTSTRAP_REQUIRED_DEPENDENCIES) {
160− const dependencyPath = path.join(nodeModulesDir, ...dependency.split("/"));
165+ const dependencyPath = path.join(nodeModulesDir, ...dependency.packageName.split("/"));
161166 if (!(await pathExists(dependencyPath))) {
162167 return false;
163168 }
@@ -198,7 +203,7 @@ async function ensureProjectDependencies(info: ManagedRuntimeInfo, projectDir: s · 34 unmodified lines
198203 "--omit",
199204 "dev",
200205 "--ignore-scripts",
201− ...BOOTSTRAP_REQUIRED_DEPENDENCIES,
206+ ...BOOTSTRAP_REQUIRED_DEPENDENCIES.map((dependency) => dependency.installSpecifier ?? dependency.packageName),
202207 ],
203208 { env, stdout: "pipe", stderr: "pipe" },
204209 );