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
5import { AppConvexProvider } from "@/lib/convex-provider";5import { AppConvexProvider } from "@/lib/convex-provider";
6import { QueryProvider } from "@/lib/query-provider";6import { QueryProvider } from "@/lib/query-provider";
7import { SessionProvider } from "@/lib/session-context";7import { SessionProvider } from "@/lib/session-context";
8import { AppErrorBoundary } from "@/components/app-error-boundary";
8import "./globals.css";9import "./globals.css";
9 10
10const inter = Inter({11const inter = Inter({
@@ -38,13 +39,15 @@ export default function RootLayout({ · 27 unmodified lines
38 enableSystem39 enableSystem
39 enableColorScheme40 enableColorScheme
40 >41 >
41 <QueryProvider>42 <AppErrorBoundary>
42 <AppConvexProvider>43 <QueryProvider>
43 <SessionProvider>44 <AppConvexProvider>
44 {children}45 <SessionProvider>
45 </SessionProvider>46 {children}
46 </AppConvexProvider>47 </SessionProvider>
47 </QueryProvider>48 </AppConvexProvider>
49 </QueryProvider>
50 </AppErrorBoundary>
48 <Toaster position="bottom-right" />51 <Toaster position="bottom-right" />
49 </ThemeProvider>52 </ThemeProvider>
50 </body>53 </body>
executor/apps/web/src/components/app-error-boundary.tsx+119−0
@@ -0,0 +1,119 @@
1"use client";
2
3import { Component } from "react";
4import { AlertTriangle, RotateCcw } from "lucide-react";
5import type { ErrorInfo, ReactNode } from "react";
6import { Button } from "@/components/ui/button";
7import { clearAnonymousAuth } from "@/lib/anonymous-auth";
8import { clearSessionStorage } from "@/lib/session-storage";
9
10type Props = {
11 children: ReactNode;
12};
13
14type State = {
15 error: Error | null;
16 componentErrorInfo: ErrorInfo | null;
17};
18
19const CLIENT_STORAGE_PREFIX = "executor-";
20
21function 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
48function 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
62export 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
16 functions: z.string().optional(),16 functions: z.string().optional(),
17});17});
18 18
19const BOOTSTRAP_REQUIRED_DEPENDENCIES = [19type BootstrapDependency = {
20 "convex",20 packageName: string;
21 "@convex-dev/migrations",21 installSpecifier?: string;
22 "@convex-dev/stripe",22};
23 "@convex-dev/workos-authkit",23
24 "@apidevtools/swagger-parser",24const BOOTSTRAP_REQUIRED_DEPENDENCIES: BootstrapDependency[] = [
25 "@workos-inc/node",25 { packageName: "convex" },
26 "convex-helpers",26 { packageName: "@convex-dev/migrations" },
27 "jose",27 { packageName: "@convex-dev/stripe" },
28 "openapi-typescript",28 { packageName: "@convex-dev/workos-authkit" },
29 "zod",29 { packageName: "@apidevtools/swagger-parser" },
30 "graphql",30 { packageName: "@workos-inc/node" },
31 "better-result",31 { packageName: "convex-helpers" },
32 "@modelcontextprotocol/sdk",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
35async function generateSelfHostedAdminKey(info: ManagedRuntimeInfo): Promise<string> {40async 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 for (const dependency of BOOTSTRAP_REQUIRED_DEPENDENCIES) {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 if (!(await pathExists(dependencyPath))) {166 if (!(await pathExists(dependencyPath))) {
162 return false;167 return false;
163 }168 }
@@ -198,7 +203,7 @@ async function ensureProjectDependencies(info: ManagedRuntimeInfo, projectDir: s · 34 unmodified lines
198 "--omit",203 "--omit",
199 "dev",204 "dev",
200 "--ignore-scripts",205 "--ignore-scripts",
201 ...BOOTSTRAP_REQUIRED_DEPENDENCIES,206 ...BOOTSTRAP_REQUIRED_DEPENDENCIES.map((dependency) => dependency.installSpecifier ?? dependency.packageName),
202 ],207 ],
203 { env, stdout: "pipe", stderr: "pipe" },208 { env, stdout: "pipe", stderr: "pipe" },
204 );209 );