@ivogt/rsc-router 0.0.0-experimental.1

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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
package/src/client.tsx ADDED
@@ -0,0 +1,609 @@
1
+ "use client";
2
+
3
+ import {
4
+ Component,
5
+ createElement,
6
+ useContext,
7
+ useMemo,
8
+ Suspense,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { OutletContext, type OutletContextValue } from "./outlet-context.js";
12
+ import {
13
+ type ClientErrorBoundaryFallbackProps,
14
+ type ErrorInfo,
15
+ type LoaderDefinition,
16
+ type LoaderFn,
17
+ type ResolvedSegment,
18
+ } from "./types";
19
+ import {
20
+ RouteContentWrapper,
21
+ LoaderBoundary,
22
+ } from "./route-content-wrapper.js";
23
+
24
+ /**
25
+ * Outlet component - renders child content in layouts
26
+ *
27
+ * If the current segment defines a loading component, the outlet content
28
+ * is wrapped in Suspense with the loading component as fallback.
29
+ * This means during navigation/streaming, React's Suspense will automatically
30
+ * show the loading skeleton until the content is ready.
31
+ *
32
+ * When a name prop is provided (e.g., "@modal"), renders content from
33
+ * the parallel segment with that slot name instead of the default content.
34
+ * This is used for parallel routes and intercepting routes.
35
+ *
36
+ * @param name - Optional slot name for parallel/intercept content (must start with @)
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function BlogLayout() {
41
+ * return (
42
+ * <div>
43
+ * <h1>Blog</h1>
44
+ * <Outlet />
45
+ * </div>
46
+ * );
47
+ * }
48
+ *
49
+ * // With named slot for modal/parallel content:
50
+ * function KanbanLayout() {
51
+ * return (
52
+ * <div>
53
+ * <KanbanBoard />
54
+ * <Outlet name="@modal" />
55
+ * <Outlet />
56
+ * </div>
57
+ * );
58
+ * }
59
+ * ```
60
+ */
61
+ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
62
+ const context = useContext(OutletContext);
63
+
64
+ // If name provided, render parallel/intercept content for that slot
65
+ if (name) {
66
+ const segment = context?.parallel?.find((seg) => seg.slot === name) ?? null;
67
+
68
+ if (!segment) return null;
69
+
70
+ // Determine the content to render
71
+ let content: ReactNode;
72
+ if (segment.loading || segment.component instanceof Promise) {
73
+ // Use RouteContentWrapper to handle Suspense wrapping properly
74
+ content = (
75
+ <RouteContentWrapper
76
+ content={
77
+ segment.component instanceof Promise
78
+ ? segment.component
79
+ : Promise.resolve(segment.component)
80
+ }
81
+ fallback={segment.loading}
82
+ />
83
+ );
84
+ } else {
85
+ content = segment.component ?? null;
86
+ }
87
+
88
+ // If segment has a layout, wrap appropriately
89
+ if (segment.layout) {
90
+ // Check if this segment has loaders that need streaming
91
+ // The layout renders immediately, LoaderBoundary becomes the outlet content
92
+ // When layout renders <Outlet />, it gets the LoaderBoundary which suspends
93
+ if (segment.loaderDataPromise && segment.loaderIds) {
94
+ const loaderAwareContent = (
95
+ <LoaderBoundary
96
+ loaderDataPromise={segment.loaderDataPromise}
97
+ loaderIds={segment.loaderIds}
98
+ fallback={segment.loading}
99
+ outletKey={segment.id + "-loader"}
100
+ outletContent={null}
101
+ segment={segment}
102
+ >
103
+ {content}
104
+ </LoaderBoundary>
105
+ );
106
+
107
+ return (
108
+ <OutletProvider content={loaderAwareContent} segment={segment}>
109
+ {segment.layout}
110
+ </OutletProvider>
111
+ );
112
+ }
113
+
114
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
115
+ return (
116
+ <OutletProvider content={content} segment={segment}>
117
+ {segment.layout}
118
+ </OutletProvider>
119
+ );
120
+ }
121
+
122
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
123
+ // This is common for intercept routes that use useLoader without a custom layout
124
+ if (segment.loaderDataPromise && segment.loaderIds) {
125
+ return (
126
+ <LoaderBoundary
127
+ loaderDataPromise={segment.loaderDataPromise}
128
+ loaderIds={segment.loaderIds}
129
+ fallback={segment.loading}
130
+ outletKey={segment.id + "-loader"}
131
+ outletContent={null}
132
+ segment={segment}
133
+ >
134
+ {content}
135
+ </LoaderBoundary>
136
+ );
137
+ }
138
+
139
+ return content;
140
+ }
141
+
142
+ // Default: render child content
143
+ const content = context?.content ?? null;
144
+
145
+ // If this segment defines a loading component, wrap outlet content with Suspense
146
+ // The loading component becomes the Suspense fallback, shown during streaming/navigation
147
+ if (context?.loading) {
148
+ return <Suspense fallback={context.loading}>{content}</Suspense>;
149
+ }
150
+
151
+ return content;
152
+ }
153
+ /**
154
+ * ParallelOutlet component - renders content for a named parallel slot
155
+ *
156
+ * If the parallel segment defines a loading component, the content
157
+ * is wrapped in Suspense with the loading component as fallback.
158
+ * This enables streaming and navigation loading states for parallels.
159
+ *
160
+ * @param name - The slot name (must start with @, e.g., "@modal", "@sidebar")
161
+ *
162
+ * @example
163
+ * ```tsx
164
+ * function DashboardLayout() {
165
+ * return (
166
+ * <div>
167
+ * <h1>Dashboard</h1>
168
+ * <ParallelOutlet name="@sidebar" />
169
+ * <ParallelOutlet name="@modal" />
170
+ * </div>
171
+ * );
172
+ * }
173
+ * ```
174
+ */
175
+ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
176
+ const context = useContext(OutletContext);
177
+ const segment = useMemo(() => {
178
+ if (!context?.parallel) return null;
179
+ return context.parallel.find((seg) => seg.slot === name) ?? null;
180
+ }, [context, name]);
181
+
182
+ if (!segment) return null;
183
+
184
+ // Determine the content to render
185
+ let content: ReactNode;
186
+ if (segment.loading || segment.component instanceof Promise) {
187
+ // Use RouteContentWrapper to handle Suspense wrapping properly
188
+ content = (
189
+ <RouteContentWrapper
190
+ content={
191
+ segment.component instanceof Promise
192
+ ? segment.component
193
+ : Promise.resolve(segment.component)
194
+ }
195
+ fallback={segment.loading}
196
+ />
197
+ );
198
+ } else {
199
+ content = segment.component ?? null;
200
+ }
201
+
202
+ // If segment has a layout, wrap appropriately
203
+ if (segment.layout) {
204
+ // Check if this segment has loaders that need streaming
205
+ // The layout renders immediately, LoaderBoundary becomes the outlet content
206
+ if (segment.loaderDataPromise && segment.loaderIds) {
207
+ const loaderAwareContent = (
208
+ <LoaderBoundary
209
+ loaderDataPromise={segment.loaderDataPromise}
210
+ loaderIds={segment.loaderIds}
211
+ fallback={segment.loading}
212
+ outletKey={segment.id + "-loader"}
213
+ outletContent={null}
214
+ segment={segment}
215
+ >
216
+ {content}
217
+ </LoaderBoundary>
218
+ );
219
+
220
+ return (
221
+ <OutletProvider content={loaderAwareContent} segment={segment}>
222
+ {segment.layout}
223
+ </OutletProvider>
224
+ );
225
+ }
226
+
227
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
228
+ return (
229
+ <OutletProvider content={content} segment={segment}>
230
+ {segment.layout}
231
+ </OutletProvider>
232
+ );
233
+ }
234
+
235
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
236
+ // This is common for intercept routes that use useLoader without a custom layout
237
+ if (segment.loaderDataPromise && segment.loaderIds) {
238
+ return (
239
+ <LoaderBoundary
240
+ loaderDataPromise={segment.loaderDataPromise}
241
+ loaderIds={segment.loaderIds}
242
+ fallback={segment.loading}
243
+ outletKey={segment.id + "-loader"}
244
+ outletContent={null}
245
+ segment={segment}
246
+ >
247
+ {content}
248
+ </LoaderBoundary>
249
+ );
250
+ }
251
+
252
+ return content;
253
+ }
254
+
255
+ /**
256
+ * Provider for outlet content - used internally by renderSegments
257
+ *
258
+ * Stores a reference to parent context so useLoader can walk up the chain
259
+ * to find loader data from parent layouts. If this segment defines a loading
260
+ * component, Outlet will wrap content with Suspense using that as fallback.
261
+ */
262
+ export function OutletProvider({
263
+ content,
264
+ parallel,
265
+ segment,
266
+ loaderData,
267
+ children,
268
+ }: {
269
+ content: ReactNode;
270
+ parallel?: ResolvedSegment[];
271
+ segment?: ResolvedSegment;
272
+ loaderData?: Record<string, any>;
273
+ children: ReactNode;
274
+ }): ReactNode {
275
+ // Get parent context to enable walking up the chain for loader lookups
276
+ const parentContext = useContext(OutletContext);
277
+
278
+ const value = useMemo(
279
+ () => ({
280
+ content,
281
+ parallel,
282
+ segment,
283
+ loaderData,
284
+ parent: parentContext,
285
+ loading: segment?.loading,
286
+ }),
287
+ [content, parallel, segment, loaderData, parentContext]
288
+ );
289
+
290
+ return (
291
+ <OutletContext.Provider value={value}>{children}</OutletContext.Provider>
292
+ );
293
+ }
294
+
295
+ /**
296
+ * Hook to access outlet content programmatically
297
+ *
298
+ * Alternative to using <Outlet /> component. Useful when you need
299
+ * direct access to the outlet content in your logic.
300
+ *
301
+ * @example
302
+ * ```tsx
303
+ * function BlogLayout() {
304
+ * const outlet = useOutlet();
305
+ * return <div><h1>Blog</h1>{outlet}</div>;
306
+ * }
307
+ * ```
308
+ */
309
+ export function useOutlet(): ReactNode {
310
+ const context = useContext(OutletContext);
311
+ return context?.content ?? null;
312
+ }
313
+
314
+ // Loader hooks - re-exported from dedicated file
315
+ export {
316
+ useLoader,
317
+ useFetchLoader,
318
+ type LoadFunction,
319
+ type UseLoaderResult,
320
+ type UseFetchLoaderResult,
321
+ type UseLoaderOptions,
322
+ } from "./use-loader.js";
323
+
324
+ /**
325
+ * Hook to access all loader data in the current context
326
+ *
327
+ * Returns a record of all loader data available in the current outlet context
328
+ * and all parent contexts. Useful for debugging or when you need access to
329
+ * multiple loaders.
330
+ *
331
+ * @returns Record of loader name to data, or empty object if no loaders
332
+ *
333
+ * @example
334
+ * ```tsx
335
+ * "use client";
336
+ * import { useLoaderData } from "rsc-router/client";
337
+ *
338
+ * export function DebugPanel() {
339
+ * const loaderData = useLoaderData();
340
+ * return <pre>{JSON.stringify(loaderData, null, 2)}</pre>;
341
+ * }
342
+ * ```
343
+ */
344
+ export function useLoaderData(): Record<string, any> {
345
+ const context = useContext(OutletContext);
346
+
347
+ // Collect all loader data from the context chain
348
+ // Child loaders override parent loaders with the same name
349
+ const result: Record<string, any> = {};
350
+ const stack: OutletContextValue[] = [];
351
+
352
+ // Build stack from current to root
353
+ let current: OutletContextValue | null | undefined = context;
354
+ while (current) {
355
+ stack.push(current);
356
+ current = current.parent;
357
+ }
358
+
359
+ // Apply from root to current (so children override parents)
360
+ for (let i = stack.length - 1; i >= 0; i--) {
361
+ const ctx = stack[i];
362
+ if (ctx.loaderData) {
363
+ Object.assign(result, ctx.loaderData);
364
+ }
365
+ }
366
+
367
+ return result;
368
+ }
369
+
370
+ /**
371
+ * Client-safe createLoader factory
372
+ *
373
+ * Creates a loader definition that can be used with useLoader().
374
+ * This is the client-side version that only stores the $$id - the function
375
+ * is ignored since loaders only execute on the server.
376
+ *
377
+ * The $$id is injected by the exposeLoaderId Vite plugin. In most cases,
378
+ * you should import the loader directly from the server file rather than
379
+ * creating a reference manually.
380
+ *
381
+ * @param fn - Loader function (ignored on client, kept for API compatibility)
382
+ * @param _fetchable - Optional fetchable flag (ignored on client)
383
+ * @param __injectedId - $$id injected by Vite plugin
384
+ *
385
+ * @example
386
+ * ```tsx
387
+ * "use client";
388
+ * import { useLoader } from "rsc-router/client";
389
+ * import { CartLoader } from "../loaders/cart"; // Import from server file
390
+ *
391
+ * export function CartIcon() {
392
+ * const cart = useLoader(CartLoader);
393
+ * return <span>Cart ({cart?.items.length ?? 0})</span>;
394
+ * }
395
+ * ```
396
+ */
397
+ // Overload 1: With function only (not fetchable)
398
+ export function createLoader<T>(
399
+ fn: LoaderFn<T, Record<string, string | undefined>, any>
400
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
401
+
402
+ // Overload 2: With function and fetchable flag
403
+ export function createLoader<T>(
404
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
405
+ fetchable: true
406
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
407
+
408
+ // Implementation - function is ignored at runtime on client
409
+ // The $$id is injected by Vite plugin as hidden third parameter
410
+ export function createLoader(
411
+ _fn: LoaderFn<any, Record<string, string | undefined>, any>,
412
+ _fetchable?: true,
413
+ __injectedId?: string
414
+ ): LoaderDefinition<any, Record<string, string | undefined>> {
415
+ return {
416
+ __brand: "loader",
417
+ $$id: __injectedId || "",
418
+ };
419
+ }
420
+
421
+ /**
422
+ * Props for the ErrorBoundary component
423
+ */
424
+ export interface ErrorBoundaryProps {
425
+ /** Fallback UI to show when an error is caught */
426
+ fallback:
427
+ | ReactNode
428
+ | ((props: ClientErrorBoundaryFallbackProps) => ReactNode);
429
+ /** Children to render */
430
+ children: ReactNode;
431
+ /** Optional callback when an error is caught */
432
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
433
+ }
434
+
435
+ interface ErrorBoundaryState {
436
+ hasError: boolean;
437
+ error: Error | null;
438
+ }
439
+
440
+ /**
441
+ * Client-side ErrorBoundary component
442
+ *
443
+ * Catches JavaScript errors in child components during rendering,
444
+ * in lifecycle methods, and in constructors of the whole tree below them.
445
+ * Displays a fallback UI instead of the component tree that crashed.
446
+ *
447
+ * Use this to wrap client components that might throw during hydration
448
+ * or user interaction. For server-side errors (middleware, loaders, handlers),
449
+ * use the errorBoundary() helper in route definitions instead.
450
+ *
451
+ * @example
452
+ * ```tsx
453
+ * "use client";
454
+ * import { ErrorBoundary } from "rsc-router/client";
455
+ *
456
+ * function MyComponent() {
457
+ * return (
458
+ * <ErrorBoundary fallback={<div>Something went wrong</div>}>
459
+ * <ComponentThatMightThrow />
460
+ * </ErrorBoundary>
461
+ * );
462
+ * }
463
+ *
464
+ * // Or with a function fallback for more control:
465
+ * function MyComponent() {
466
+ * return (
467
+ * <ErrorBoundary
468
+ * fallback={({ error, reset }) => (
469
+ * <div>
470
+ * <p>Error: {error.message}</p>
471
+ * <button onClick={reset}>Try again</button>
472
+ * </div>
473
+ * )}
474
+ * >
475
+ * <ComponentThatMightThrow />
476
+ * </ErrorBoundary>
477
+ * );
478
+ * }
479
+ * ```
480
+ */
481
+ export class ErrorBoundary extends Component<
482
+ ErrorBoundaryProps,
483
+ ErrorBoundaryState
484
+ > {
485
+ constructor(props: ErrorBoundaryProps) {
486
+ super(props);
487
+ this.state = { hasError: false, error: null };
488
+ }
489
+
490
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
491
+ return { hasError: true, error };
492
+ }
493
+
494
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
495
+ console.error("[ErrorBoundary] Error caught:", error, errorInfo);
496
+ this.props.onError?.(error, errorInfo);
497
+ }
498
+
499
+ reset = (): void => {
500
+ this.setState({ hasError: false, error: null });
501
+ };
502
+
503
+ render(): ReactNode {
504
+ if (this.state.hasError && this.state.error) {
505
+ const { fallback } = this.props;
506
+
507
+ // Create error info for the fallback
508
+ const errorInfo: ErrorInfo = {
509
+ message: this.state.error.message,
510
+ name: this.state.error.name,
511
+ stack: this.state.error.stack,
512
+ cause: this.state.error.cause,
513
+ segmentId: "client",
514
+ segmentType: "route",
515
+ };
516
+
517
+ // Render fallback - use createElement so hooks work in function fallbacks
518
+ if (typeof fallback === "function") {
519
+ return createElement(fallback, { error: errorInfo, reset: this.reset });
520
+ }
521
+
522
+ return fallback;
523
+ }
524
+
525
+ return this.props.children;
526
+ }
527
+ }
528
+
529
+ // ============================================================================
530
+ // Re-exports from browser/react for convenience
531
+ // These are the most commonly used client-side navigation utilities
532
+ // ============================================================================
533
+
534
+ // Navigation hooks
535
+ export {
536
+ useNavigation,
537
+ type NavigationMethods,
538
+ type NavigationValue,
539
+ } from "./browser/react/use-navigation.js";
540
+
541
+ // Action state tracking hook
542
+ export {
543
+ useAction,
544
+ type ServerActionFunction,
545
+ } from "./browser/react/use-action.js";
546
+
547
+ // Segments state hook
548
+ export {
549
+ useSegments,
550
+ type SegmentsState,
551
+ } from "./browser/react/use-segments.js";
552
+
553
+ // Client cache controls hook
554
+ export {
555
+ useClientCache,
556
+ type ClientCacheControls,
557
+ } from "./browser/react/use-client-cache.js";
558
+
559
+ // Provider
560
+ export {
561
+ NavigationProvider,
562
+ type NavigationProviderProps,
563
+ } from "./browser/react/NavigationProvider.js";
564
+
565
+ // Link component
566
+ export {
567
+ Link,
568
+ type LinkProps,
569
+ type PrefetchStrategy,
570
+ type StateOrGetter,
571
+ } from "./browser/react/Link.js";
572
+
573
+ // Link status hook
574
+ export {
575
+ useLinkStatus,
576
+ type LinkStatus,
577
+ } from "./browser/react/use-link-status.js";
578
+
579
+ // Scroll restoration
580
+ export {
581
+ ScrollRestoration,
582
+ useScrollRestoration,
583
+ type ScrollRestorationProps,
584
+ } from "./browser/react/ScrollRestoration.js";
585
+
586
+ // Handle API - for accumulating data across route segments
587
+ export { createHandle, isHandle, type Handle } from "./handle.js";
588
+
589
+ // Handle data hook
590
+ export { useHandle } from "./browser/react/use-handle.js";
591
+
592
+ // Built-in handles
593
+ export { Meta } from "./handles/meta.js";
594
+ export { MetaTags } from "./handles/MetaTags.js";
595
+ export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
596
+
597
+ // Location state - type-safe navigation state
598
+ export {
599
+ createLocationState,
600
+ useLocationState,
601
+ type LocationStateDefinition,
602
+ type LocationStateEntry,
603
+ } from "./browser/react/location-state.js";
604
+
605
+ // Type-safe href for client-side path validation
606
+ export { href, type ValidPaths, type PatternToPath } from "./href-client.js";
607
+
608
+ // Loader definition type - for typing loader props in client components
609
+ export type { LoaderDefinition } from "./types.js";
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, ReactElement } from "react";
4
+ import { MetaTags } from "../handles/MetaTags.js";
5
+
6
+ /**
7
+ * Default document component that provides a basic HTML structure.
8
+ * Used when no custom document is provided to createRSCRouter.
9
+ * Includes MetaTags for automatic charset, viewport, and route meta support.
10
+ */
11
+ export function DefaultDocument({ children }: { children: ReactNode }): ReactElement {
12
+ return (
13
+ <html lang="en">
14
+ <head>
15
+ <MetaTags />
16
+ </head>
17
+ <body>{children}</body>
18
+ </html>
19
+ );
20
+ }
@@ -0,0 +1,88 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ErrorBoundaryFallbackProps } from "./types.js";
3
+
4
+ /**
5
+ * Default error boundary fallback component
6
+ *
7
+ * This is rendered when an error occurs and no custom error boundary
8
+ * is defined in the route tree. Shows a simple "Internal Server Error"
9
+ * message with the error details in development.
10
+ */
11
+ export function DefaultErrorFallback({
12
+ error,
13
+ }: ErrorBoundaryFallbackProps): ReactNode {
14
+ const isDev = process.env.NODE_ENV !== "production";
15
+
16
+ return (
17
+ <div
18
+ style={{
19
+ fontFamily: "system-ui, -apple-system, sans-serif",
20
+ padding: "2rem",
21
+ maxWidth: "600px",
22
+ margin: "2rem auto",
23
+ }}
24
+ >
25
+ <h1
26
+ style={{
27
+ color: "#dc2626",
28
+ fontSize: "1.5rem",
29
+ marginBottom: "1rem",
30
+ }}
31
+ >
32
+ Internal Server Error
33
+ </h1>
34
+ <p
35
+ style={{
36
+ color: "#374151",
37
+ marginBottom: "1rem",
38
+ }}
39
+ >
40
+ An unexpected error occurred while processing your request.
41
+ </p>
42
+ {isDev && (
43
+ <div
44
+ style={{
45
+ background: "#fef2f2",
46
+ border: "1px solid #fecaca",
47
+ borderRadius: "0.5rem",
48
+ padding: "1rem",
49
+ marginBottom: "1rem",
50
+ }}
51
+ >
52
+ <p
53
+ style={{
54
+ fontWeight: 600,
55
+ color: "#991b1b",
56
+ marginBottom: "0.5rem",
57
+ }}
58
+ >
59
+ {error.name}: {error.message}
60
+ </p>
61
+ {error.stack && (
62
+ <pre
63
+ style={{
64
+ fontSize: "0.75rem",
65
+ color: "#6b7280",
66
+ overflow: "auto",
67
+ whiteSpace: "pre-wrap",
68
+ wordBreak: "break-word",
69
+ }}
70
+ >
71
+ {error.stack}
72
+ </pre>
73
+ )}
74
+ </div>
75
+ )}
76
+ <a
77
+ href="/"
78
+ style={{
79
+ display: "inline-block",
80
+ color: "#2563eb",
81
+ textDecoration: "underline",
82
+ }}
83
+ >
84
+ Go to homepage
85
+ </a>
86
+ </div>
87
+ );
88
+ }