@rangojs/router 0.0.0-experimental.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
package/src/reverse.ts ADDED
@@ -0,0 +1,267 @@
1
+ import type { ExtractParams } from "./types.js";
2
+
3
+ /**
4
+ * Sanitize prefix string by removing leading slash
5
+ * "/shop" -> "shop", "blog" -> "blog", "" -> ""
6
+ */
7
+ export type SanitizePrefix<T extends string> = T extends `/${infer P}` ? P : T;
8
+
9
+ /**
10
+ * Helper type to merge multiple route definitions into a single accumulated type.
11
+ * Note: When using createRouter, types accumulate automatically through the
12
+ * builder chain, so this type is typically not needed.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // Manual type merging (rarely needed):
17
+ * type AppRoutes = MergeRoutes<[
18
+ * typeof homeRoutes,
19
+ * PrefixRoutePatterns<typeof blogRoutes, "/blog">,
20
+ * ]>;
21
+ *
22
+ * // Preferred: Let router accumulate types automatically
23
+ * const router = createRouter<AppEnv>()
24
+ * .routes(homeRoutes).map(...)
25
+ * .routes("/blog", blogRoutes).map(...);
26
+ * type AppRoutes = typeof router.routeMap;
27
+ * ```
28
+ */
29
+ export type MergeRoutes<T extends unknown[]> = T extends [
30
+ infer First,
31
+ ...infer Rest
32
+ ]
33
+ ? First & MergeRoutes<Rest>
34
+ : {};
35
+
36
+ /**
37
+ * Add key prefix to all entries in a route map
38
+ * { "cart": "/cart" } with prefix "shop" -> { "shop.cart": "/shop/cart" }
39
+ */
40
+ export type PrefixRouteKeys<
41
+ T,
42
+ Prefix extends string
43
+ > = Prefix extends ""
44
+ ? T
45
+ : { [K in keyof T as `${Prefix}.${K & string}`]: T[K] };
46
+
47
+ /**
48
+ * Add path prefix to all patterns in a route map
49
+ * { "cart": "/cart" } with prefix "/shop" -> { "cart": "/shop/cart" }
50
+ */
51
+ export type PrefixRoutePatterns<
52
+ T,
53
+ PathPrefix extends string
54
+ > = {
55
+ [K in keyof T]: PathPrefix extends "" | "/"
56
+ ? T[K]
57
+ : T[K] extends "/"
58
+ ? PathPrefix
59
+ : T[K] extends string
60
+ ? `${PathPrefix}${T[K]}`
61
+ : T[K];
62
+ };
63
+
64
+ /**
65
+ * Combined: prefix both keys and patterns
66
+ * Used for module augmentation registration
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * // Given shopRoutes = { "index": "/", "cart": "/cart", "products.detail": "/product/:slug" }
71
+ * // PrefixedRoutes<typeof shopRoutes, "shop"> produces:
72
+ * // { "shop.index": "/shop", "shop.cart": "/shop/cart", "shop.products.detail": "/shop/product/:slug" }
73
+ * ```
74
+ */
75
+ export type PrefixedRoutes<
76
+ T,
77
+ KeyPrefix extends string,
78
+ PathPrefix extends string = KeyPrefix extends "" ? "" : `/${KeyPrefix}`
79
+ > = PrefixRouteKeys<PrefixRoutePatterns<T, PathPrefix>, KeyPrefix>;
80
+
81
+ /**
82
+ * Helper to safely extract route patterns from a routes object
83
+ * Handles string values, { path, response } objects, and interface types (like RegisteredRoutes)
84
+ */
85
+ type RoutePatternFor<TRoutes, TName extends keyof TRoutes> =
86
+ TRoutes[TName] extends string ? TRoutes[TName]
87
+ : TRoutes[TName] extends { readonly path: infer P extends string } ? P
88
+ : string;
89
+
90
+ /**
91
+ * Extract params type for a route
92
+ */
93
+ export type ParamsFor<
94
+ TRoutes,
95
+ TName extends keyof TRoutes
96
+ > = ExtractParams<RoutePatternFor<TRoutes, TName>>;
97
+
98
+ /**
99
+ * Check if an object type has any keys
100
+ */
101
+ type IsEmptyObject<T> = keyof T extends never ? true : false;
102
+
103
+ /**
104
+ * Type-safe reverse function signature (Django-style URL reversal)
105
+ *
106
+ * Validates route names and params at compile time.
107
+ * Use route names instead of raw paths for full type safety.
108
+ *
109
+ * @example
110
+ * ```typescript
111
+ * reverse("cart") // ✓ Validates route exists
112
+ * reverse("product.detail", { id: "123" }) // ✓ Validates route + params
113
+ * ```
114
+ */
115
+ export type ReverseFunction<TRoutes> = {
116
+ /**
117
+ * Route without params - validates route name exists
118
+ */
119
+ <TName extends keyof TRoutes & string>(
120
+ name: IsEmptyObject<ExtractParams<RoutePatternFor<TRoutes, TName>>> extends true ? TName : never
121
+ ): string;
122
+
123
+ /**
124
+ * Route with params - validates both route name and params
125
+ */
126
+ <TName extends keyof TRoutes & string>(
127
+ name: TName,
128
+ params: ExtractParams<RoutePatternFor<TRoutes, TName>>
129
+ ): string;
130
+ };
131
+
132
+ /**
133
+ * Type-safe scoped reverse function signature for use with scopedReverse<typeof patterns>()
134
+ *
135
+ * **Recommended: Use route names for type safety.**
136
+ * Route names validate both the route exists and params are correct.
137
+ * Path-based URLs (`/...`) are an escape hatch with no validation.
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * // RECOMMENDED: Use route names for type safety
142
+ * reverse("blog.post", { slug: "hello" }) // ✓ Validates route + params
143
+ * reverse("shop.cart") // ✓ Validates route exists
144
+ *
145
+ * // ESCAPE HATCH: Path-based URLs (no validation)
146
+ * reverse("/about") // ⚠ No type checking
147
+ * reverse("/typo/in/path") // ⚠ Won't catch errors
148
+ * ```
149
+ */
150
+ export type ScopedReverseFunction<TLocalRoutes> = {
151
+ /**
152
+ * Route without params - validates route name exists
153
+ * @recommended Use this for type-safe URL generation
154
+ */
155
+ <TName extends keyof TLocalRoutes & string>(
156
+ name: IsEmptyObject<ExtractParams<RoutePatternFor<TLocalRoutes, TName>>> extends true ? TName : never
157
+ ): string;
158
+
159
+ /**
160
+ * Route with params - validates both route name and params
161
+ * @recommended Use this for type-safe URL generation with parameters
162
+ */
163
+ <TName extends keyof TLocalRoutes & string>(
164
+ name: TName,
165
+ params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>
166
+ ): string;
167
+
168
+ /**
169
+ * Absolute route name (contains dot) - global lookup
170
+ * Use for cross-module navigation: "shop.cart", "blog.post"
171
+ */
172
+ (name: `${string}.${string}`, params?: Record<string, string>): string;
173
+
174
+ /**
175
+ * Path-based URL - ESCAPE HATCH, no type validation
176
+ * Prefer route names for type safety. Only use paths when necessary.
177
+ */
178
+ (name: `/${string}`, params?: Record<string, string>): string;
179
+ };
180
+
181
+ /**
182
+ * Extract local routes type from UrlPatterns
183
+ * Used with scopedReverse() to get the routes type from patterns
184
+ */
185
+ export type ExtractLocalRoutes<TPatterns> =
186
+ TPatterns extends { readonly _routes?: infer TRoutes }
187
+ ? TRoutes
188
+ : TPatterns extends Record<string, string>
189
+ ? TPatterns
190
+ : Record<string, string>;
191
+
192
+ /**
193
+ * Extract the response data type for a named route from a UrlPatterns instance.
194
+ * Re-exported from urls.ts for consumer convenience.
195
+ */
196
+ export type { RouteResponse } from "./urls.js";
197
+
198
+ /**
199
+ * Get a locally-typed reverse function from ctx.reverse for composable modules.
200
+ *
201
+ * This is a type-only cast - ctx.reverse already resolves local names at runtime
202
+ * based on the current route prefix. This helper just provides type safety
203
+ * for local route names within a url module.
204
+ *
205
+ * @param reverse - The ctx.reverse function from HandlerContext
206
+ * @returns The same reverse function, but typed for local routes
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * // urls/blog.tsx
211
+ * export const blogPatterns = urls(({ path }) => [
212
+ * path("/", (ctx) => {
213
+ * // Get locally-typed reverse for this module's routes
214
+ * const reverse = scopedReverse<typeof blogPatterns>(ctx.reverse);
215
+ *
216
+ * reverse("index"); // ✓ Type-safe local route
217
+ * reverse("post", { slug: "x" }); // ✓ Type-safe with params
218
+ * reverse("shop.cart"); // ✓ Cross-module (absolute name)
219
+ *
220
+ * return <BlogIndex />;
221
+ * }, { name: "index" }),
222
+ *
223
+ * path("/:slug", BlogPost, { name: "post" }),
224
+ * ]);
225
+ * ```
226
+ */
227
+ export function scopedReverse<TPatterns>(
228
+ reverse: ((...args: any[]) => string)
229
+ ): ScopedReverseFunction<ExtractLocalRoutes<TPatterns>> {
230
+ return reverse as ScopedReverseFunction<ExtractLocalRoutes<TPatterns>>;
231
+ }
232
+
233
+ /**
234
+ * Create a type-safe reverse function for URL generation
235
+ *
236
+ * @param routeMap - Flattened route map with all registered routes
237
+ * @returns Type-safe reverse function
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" }
242
+ * const reverse = createReverse(routeMap);
243
+ * reverse("cart"); // "/shop/cart"
244
+ * reverse("detail", { slug: "my-product" }); // "/shop/product/my-product"
245
+ * ```
246
+ */
247
+ export function createReverse<TRoutes extends Record<string, string>>(
248
+ routeMap: TRoutes
249
+ ): ReverseFunction<TRoutes & Record<string, string>> {
250
+ return ((name: string, params?: Record<string, string>) => {
251
+ const pattern = routeMap[name];
252
+ if (!pattern) {
253
+ throw new Error(`Unknown route: ${name}`);
254
+ }
255
+
256
+ if (!params) return pattern;
257
+
258
+ // Replace :param placeholders with actual values
259
+ return pattern.replace(/:([^/]+)/g, (_, key) => {
260
+ const value = params[key];
261
+ if (value === undefined) {
262
+ throw new Error(`Missing param "${key}" for route "${name}"`);
263
+ }
264
+ return encodeURIComponent(value);
265
+ });
266
+ }) as ReverseFunction<TRoutes>;
267
+ }
@@ -0,0 +1,277 @@
1
+ "use client";
2
+
3
+ import { Component, useState, type ReactNode } from "react";
4
+ import type { ClientErrorBoundaryFallbackProps } from "./types.js";
5
+
6
+ /**
7
+ * Check if an error is a network-related error
8
+ */
9
+ function isNetworkError(error: Error): boolean {
10
+ return error.name === "NetworkError";
11
+ }
12
+
13
+ /**
14
+ * Network error fallback UI with retry functionality
15
+ * Shows a connection-specific message and allows retrying via page refresh
16
+ */
17
+ function NetworkErrorFallback({
18
+ error,
19
+ reset,
20
+ }: ClientErrorBoundaryFallbackProps): ReactNode {
21
+ const [isRetrying, setIsRetrying] = useState(false);
22
+
23
+ const handleRetry = (): void => {
24
+ setIsRetrying(true);
25
+ // Refresh the page to retry the request
26
+ window.location.reload();
27
+ };
28
+
29
+ return (
30
+ <div
31
+ style={{
32
+ fontFamily: "system-ui, -apple-system, sans-serif",
33
+ padding: "2rem",
34
+ maxWidth: "600px",
35
+ margin: "2rem auto",
36
+ textAlign: "center",
37
+ }}
38
+ >
39
+ <div
40
+ style={{
41
+ fontSize: "3rem",
42
+ marginBottom: "1rem",
43
+ }}
44
+ >
45
+ {/* Simple cloud with x icon using CSS */}
46
+ <span style={{ color: "#9ca3af" }}>&#9729;</span>
47
+ </div>
48
+ <h1
49
+ style={{
50
+ color: "#374151",
51
+ fontSize: "1.5rem",
52
+ marginBottom: "0.5rem",
53
+ }}
54
+ >
55
+ Connection Error
56
+ </h1>
57
+ <p
58
+ style={{
59
+ color: "#6b7280",
60
+ marginBottom: "1.5rem",
61
+ }}
62
+ >
63
+ {error.message || "Unable to connect to the server. Please check your internet connection."}
64
+ </p>
65
+ <div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
66
+ <button
67
+ type="button"
68
+ onClick={handleRetry}
69
+ disabled={isRetrying}
70
+ style={{
71
+ padding: "0.75rem 1.5rem",
72
+ backgroundColor: isRetrying ? "#9ca3af" : "#2563eb",
73
+ color: "white",
74
+ border: "none",
75
+ borderRadius: "0.375rem",
76
+ cursor: isRetrying ? "not-allowed" : "pointer",
77
+ fontSize: "1rem",
78
+ fontWeight: 500,
79
+ }}
80
+ >
81
+ {isRetrying ? "Retrying..." : "Retry"}
82
+ </button>
83
+ <button
84
+ type="button"
85
+ onClick={() => window.history.back()}
86
+ style={{
87
+ padding: "0.75rem 1.5rem",
88
+ backgroundColor: "transparent",
89
+ color: "#6b7280",
90
+ border: "1px solid #d1d5db",
91
+ borderRadius: "0.375rem",
92
+ cursor: "pointer",
93
+ fontSize: "1rem",
94
+ }}
95
+ >
96
+ Go Back
97
+ </button>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Default fallback UI for root error boundary
105
+ * This is shown when an unhandled error bubbles up to the root
106
+ */
107
+ function RootErrorFallback({ error, reset }: ClientErrorBoundaryFallbackProps): ReactNode {
108
+ return (
109
+ <div
110
+ style={{
111
+ fontFamily: "system-ui, -apple-system, sans-serif",
112
+ padding: "2rem",
113
+ maxWidth: "600px",
114
+ margin: "2rem auto",
115
+ }}
116
+ >
117
+ <h1
118
+ style={{
119
+ color: "#dc2626",
120
+ fontSize: "1.5rem",
121
+ marginBottom: "1rem",
122
+ }}
123
+ >
124
+ Internal Server Error
125
+ </h1>
126
+ <p
127
+ style={{
128
+ color: "#374151",
129
+ marginBottom: "1rem",
130
+ }}
131
+ >
132
+ An unexpected error occurred while processing your request.
133
+ </p>
134
+ <div
135
+ style={{
136
+ background: "#fef2f2",
137
+ border: "1px solid #fecaca",
138
+ borderRadius: "0.5rem",
139
+ padding: "1rem",
140
+ marginBottom: "1rem",
141
+ }}
142
+ >
143
+ <p
144
+ style={{
145
+ fontWeight: 600,
146
+ color: "#991b1b",
147
+ marginBottom: "0.5rem",
148
+ }}
149
+ >
150
+ {error.name}: {error.message}
151
+ </p>
152
+ {error.stack && (
153
+ <pre
154
+ style={{
155
+ fontSize: "0.75rem",
156
+ color: "#6b7280",
157
+ overflow: "auto",
158
+ whiteSpace: "pre-wrap",
159
+ wordBreak: "break-word",
160
+ }}
161
+ >
162
+ {error.stack}
163
+ </pre>
164
+ )}
165
+ </div>
166
+ <div style={{ display: "flex", gap: "1rem" }}>
167
+ <button
168
+ type="button"
169
+ onClick={reset}
170
+ style={{
171
+ padding: "0.5rem 1rem",
172
+ backgroundColor: "#2563eb",
173
+ color: "white",
174
+ border: "none",
175
+ borderRadius: "0.25rem",
176
+ cursor: "pointer",
177
+ }}
178
+ >
179
+ Try Again
180
+ </button>
181
+ <a
182
+ href="/"
183
+ style={{
184
+ display: "inline-block",
185
+ padding: "0.5rem 1rem",
186
+ color: "#2563eb",
187
+ textDecoration: "underline",
188
+ }}
189
+ >
190
+ Go to homepage
191
+ </a>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ interface RootErrorBoundaryState {
198
+ hasError: boolean;
199
+ error: Error | null;
200
+ }
201
+
202
+ /**
203
+ * Root error boundary component
204
+ *
205
+ * Wraps the entire segment tree to catch any unhandled errors that bubble up.
206
+ * This prevents the entire app from crashing with a white screen.
207
+ *
208
+ * This is a client component with an inline fallback to avoid the
209
+ * "Functions cannot be passed to Client Components" RSC error.
210
+ */
211
+ export class RootErrorBoundary extends Component<
212
+ { children: ReactNode },
213
+ RootErrorBoundaryState
214
+ > {
215
+ constructor(props: { children: ReactNode }) {
216
+ super(props);
217
+ this.state = { hasError: false, error: null };
218
+ }
219
+
220
+ static getDerivedStateFromError(error: Error): RootErrorBoundaryState {
221
+ return { hasError: true, error };
222
+ }
223
+
224
+ componentDidMount(): void {
225
+ // Listen for popstate (back/forward navigation) to reset error state
226
+ window.addEventListener("popstate", this.handlePopState);
227
+ }
228
+
229
+ componentWillUnmount(): void {
230
+ window.removeEventListener("popstate", this.handlePopState);
231
+ }
232
+
233
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
234
+ console.error("[RootErrorBoundary] Unhandled error caught:", error, errorInfo);
235
+ }
236
+
237
+ componentDidUpdate(prevProps: { children: ReactNode }): void {
238
+ // Reset error state when children change (e.g., navigation)
239
+ // This allows the app to recover after navigation away from an errored route
240
+ if (this.state.hasError && prevProps.children !== this.props.children) {
241
+ this.setState({ hasError: false, error: null });
242
+ }
243
+ }
244
+
245
+ handlePopState = (): void => {
246
+ // Reset error state on back/forward navigation
247
+ if (this.state.hasError) {
248
+ this.setState({ hasError: false, error: null });
249
+ }
250
+ };
251
+
252
+ reset = (): void => {
253
+ this.setState({ hasError: false, error: null });
254
+ };
255
+
256
+ render(): ReactNode {
257
+ if (this.state.hasError && this.state.error) {
258
+ const errorInfo = {
259
+ message: this.state.error.message,
260
+ name: this.state.error.name,
261
+ stack: this.state.error.stack,
262
+ cause: this.state.error.cause,
263
+ segmentId: "root",
264
+ segmentType: "route" as const,
265
+ };
266
+
267
+ // Use specialized fallback for network errors
268
+ if (isNetworkError(this.state.error)) {
269
+ return <NetworkErrorFallback error={errorInfo} reset={this.reset} />;
270
+ }
271
+
272
+ return <RootErrorFallback error={errorInfo} reset={this.reset} />;
273
+ }
274
+
275
+ return this.props.children;
276
+ }
277
+ }