@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.
- package/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- 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" }}>☁</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
|
+
}
|