@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/client.tsx ADDED
@@ -0,0 +1,643 @@
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
+ segmentId={segment.id}
83
+ />
84
+ );
85
+ } else {
86
+ content = segment.component ?? null;
87
+ }
88
+
89
+ // If segment has a layout, wrap appropriately
90
+ if (segment.layout) {
91
+ // Check if this segment has loaders that need streaming
92
+ // The layout renders immediately, LoaderBoundary becomes the outlet content
93
+ // When layout renders <Outlet />, it gets the LoaderBoundary which suspends
94
+ if (segment.loaderDataPromise && segment.loaderIds) {
95
+ const loaderAwareContent = (
96
+ <LoaderBoundary
97
+ loaderDataPromise={segment.loaderDataPromise}
98
+ loaderIds={segment.loaderIds}
99
+ fallback={segment.loading}
100
+ outletKey={segment.id + "-loader"}
101
+ outletContent={null}
102
+ segment={segment}
103
+ >
104
+ {content}
105
+ </LoaderBoundary>
106
+ );
107
+
108
+ return (
109
+ <OutletProvider content={loaderAwareContent} segment={segment}>
110
+ {segment.layout}
111
+ </OutletProvider>
112
+ );
113
+ }
114
+
115
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
116
+ return (
117
+ <OutletProvider content={content} segment={segment}>
118
+ {segment.layout}
119
+ </OutletProvider>
120
+ );
121
+ }
122
+
123
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
124
+ // This is common for intercept routes that use useLoader without a custom layout
125
+ if (segment.loaderDataPromise && segment.loaderIds) {
126
+ return (
127
+ <LoaderBoundary
128
+ loaderDataPromise={segment.loaderDataPromise}
129
+ loaderIds={segment.loaderIds}
130
+ fallback={segment.loading}
131
+ outletKey={segment.id + "-loader"}
132
+ outletContent={null}
133
+ segment={segment}
134
+ >
135
+ {content}
136
+ </LoaderBoundary>
137
+ );
138
+ }
139
+
140
+ return content;
141
+ }
142
+
143
+ // Default: render child content
144
+ const content = context?.content ?? null;
145
+
146
+ // If this segment defines a loading component, wrap outlet content with Suspense
147
+ // The loading component becomes the Suspense fallback, shown during streaming/navigation
148
+ if (context?.loading) {
149
+ return <Suspense fallback={context.loading}>{content}</Suspense>;
150
+ }
151
+
152
+ return content;
153
+ }
154
+ /**
155
+ * ParallelOutlet component - renders content for a named parallel slot
156
+ *
157
+ * If the parallel segment defines a loading component, the content
158
+ * is wrapped in Suspense with the loading component as fallback.
159
+ * This enables streaming and navigation loading states for parallels.
160
+ *
161
+ * @param name - The slot name (must start with @, e.g., "@modal", "@sidebar")
162
+ *
163
+ * @example
164
+ * ```tsx
165
+ * function DashboardLayout() {
166
+ * return (
167
+ * <div>
168
+ * <h1>Dashboard</h1>
169
+ * <ParallelOutlet name="@sidebar" />
170
+ * <ParallelOutlet name="@modal" />
171
+ * </div>
172
+ * );
173
+ * }
174
+ * ```
175
+ */
176
+ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
177
+ const context = useContext(OutletContext);
178
+ const segment = useMemo(() => {
179
+ if (!context?.parallel) return null;
180
+ return context.parallel.find((seg) => seg.slot === name) ?? null;
181
+ }, [context, name]);
182
+
183
+ if (!segment) return null;
184
+
185
+ // Determine the content to render
186
+ let content: ReactNode;
187
+ if (segment.loading || segment.component instanceof Promise) {
188
+ // Use RouteContentWrapper to handle Suspense wrapping properly
189
+ content = (
190
+ <RouteContentWrapper
191
+ content={
192
+ segment.component instanceof Promise
193
+ ? segment.component
194
+ : Promise.resolve(segment.component)
195
+ }
196
+ fallback={segment.loading}
197
+ segmentId={segment.id}
198
+ />
199
+ );
200
+ } else {
201
+ content = segment.component ?? null;
202
+ }
203
+
204
+ // If segment has a layout, wrap appropriately
205
+ if (segment.layout) {
206
+ // Check if this segment has loaders that need streaming
207
+ // The layout renders immediately, LoaderBoundary becomes the outlet content
208
+ if (segment.loaderDataPromise && segment.loaderIds) {
209
+ const loaderAwareContent = (
210
+ <LoaderBoundary
211
+ loaderDataPromise={segment.loaderDataPromise}
212
+ loaderIds={segment.loaderIds}
213
+ fallback={segment.loading}
214
+ outletKey={segment.id + "-loader"}
215
+ outletContent={null}
216
+ segment={segment}
217
+ >
218
+ {content}
219
+ </LoaderBoundary>
220
+ );
221
+
222
+ return (
223
+ <OutletProvider content={loaderAwareContent} segment={segment}>
224
+ {segment.layout}
225
+ </OutletProvider>
226
+ );
227
+ }
228
+
229
+ // No loaders - wrap in OutletProvider so layout can use <Outlet />
230
+ return (
231
+ <OutletProvider content={content} segment={segment}>
232
+ {segment.layout}
233
+ </OutletProvider>
234
+ );
235
+ }
236
+
237
+ // No layout but has loaders - wrap content with LoaderBoundary for useLoader context
238
+ // This is common for intercept routes that use useLoader without a custom layout
239
+ if (segment.loaderDataPromise && segment.loaderIds) {
240
+ return (
241
+ <LoaderBoundary
242
+ loaderDataPromise={segment.loaderDataPromise}
243
+ loaderIds={segment.loaderIds}
244
+ fallback={segment.loading}
245
+ outletKey={segment.id + "-loader"}
246
+ outletContent={null}
247
+ segment={segment}
248
+ >
249
+ {content}
250
+ </LoaderBoundary>
251
+ );
252
+ }
253
+
254
+ return content;
255
+ }
256
+
257
+ /**
258
+ * Provider for outlet content - used internally by renderSegments
259
+ *
260
+ * Stores a reference to parent context so useLoader can walk up the chain
261
+ * to find loader data from parent layouts. If this segment defines a loading
262
+ * component, Outlet will wrap content with Suspense using that as fallback.
263
+ */
264
+ export function OutletProvider({
265
+ content,
266
+ parallel,
267
+ segment,
268
+ loaderData,
269
+ children,
270
+ }: {
271
+ content: ReactNode;
272
+ parallel?: ResolvedSegment[];
273
+ segment?: ResolvedSegment;
274
+ loaderData?: Record<string, any>;
275
+ children: ReactNode;
276
+ }): ReactNode {
277
+ // Get parent context to enable walking up the chain for loader lookups
278
+ const parentContext = useContext(OutletContext);
279
+
280
+ const value = useMemo(
281
+ () => ({
282
+ content,
283
+ parallel,
284
+ segment,
285
+ loaderData,
286
+ parent: parentContext,
287
+ loading: segment?.loading,
288
+ }),
289
+ [content, parallel, segment, loaderData, parentContext]
290
+ );
291
+
292
+ return (
293
+ <OutletContext.Provider value={value}>{children}</OutletContext.Provider>
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Hook to access outlet content programmatically
299
+ *
300
+ * Alternative to using <Outlet /> component. Useful when you need
301
+ * direct access to the outlet content in your logic.
302
+ *
303
+ * @example
304
+ * ```tsx
305
+ * function BlogLayout() {
306
+ * const outlet = useOutlet();
307
+ * return <div><h1>Blog</h1>{outlet}</div>;
308
+ * }
309
+ * ```
310
+ */
311
+ export function useOutlet(): ReactNode {
312
+ const context = useContext(OutletContext);
313
+ return context?.content ?? null;
314
+ }
315
+
316
+ // Loader hooks - re-exported from dedicated file
317
+ export {
318
+ useLoader,
319
+ useFetchLoader,
320
+ type LoadFunction,
321
+ type UseLoaderResult,
322
+ type UseFetchLoaderResult,
323
+ type UseLoaderOptions,
324
+ } from "./use-loader.js";
325
+
326
+ /**
327
+ * Hook to access all loader data in the current context
328
+ *
329
+ * Returns a record of all loader data available in the current outlet context
330
+ * and all parent contexts. Useful for debugging or when you need access to
331
+ * multiple loaders.
332
+ *
333
+ * @returns Record of loader name to data, or empty object if no loaders
334
+ *
335
+ * @example
336
+ * ```tsx
337
+ * "use client";
338
+ * import { useLoaderData } from "rsc-router/client";
339
+ *
340
+ * export function DebugPanel() {
341
+ * const loaderData = useLoaderData();
342
+ * return <pre>{JSON.stringify(loaderData, null, 2)}</pre>;
343
+ * }
344
+ * ```
345
+ */
346
+ export function useLoaderData(): Record<string, any> {
347
+ const context = useContext(OutletContext);
348
+
349
+ // Collect all loader data from the context chain
350
+ // Child loaders override parent loaders with the same name
351
+ const result: Record<string, any> = {};
352
+ const stack: OutletContextValue[] = [];
353
+
354
+ // Build stack from current to root
355
+ let current: OutletContextValue | null | undefined = context;
356
+ while (current) {
357
+ stack.push(current);
358
+ current = current.parent;
359
+ }
360
+
361
+ // Apply from root to current (so children override parents)
362
+ for (let i = stack.length - 1; i >= 0; i--) {
363
+ const ctx = stack[i];
364
+ if (ctx.loaderData) {
365
+ Object.assign(result, ctx.loaderData);
366
+ }
367
+ }
368
+
369
+ return result;
370
+ }
371
+
372
+ /**
373
+ * Client-safe createLoader factory
374
+ *
375
+ * Creates a loader definition that can be used with useLoader().
376
+ * This is the client-side version that only stores the $$id - the function
377
+ * is ignored since loaders only execute on the server.
378
+ *
379
+ * The $$id is injected by the exposeLoaderId Vite plugin. In most cases,
380
+ * you should import the loader directly from the server file rather than
381
+ * creating a reference manually.
382
+ *
383
+ * @param fn - Loader function (ignored on client, kept for API compatibility)
384
+ * @param _fetchable - Optional fetchable flag (ignored on client)
385
+ * @param __injectedId - $$id injected by Vite plugin
386
+ *
387
+ * @example
388
+ * ```tsx
389
+ * "use client";
390
+ * import { useLoader } from "rsc-router/client";
391
+ * import { CartLoader } from "../loaders/cart"; // Import from server file
392
+ *
393
+ * export function CartIcon() {
394
+ * const cart = useLoader(CartLoader);
395
+ * return <span>Cart ({cart?.items.length ?? 0})</span>;
396
+ * }
397
+ * ```
398
+ */
399
+ // Overload 1: With function only (not fetchable)
400
+ export function createLoader<T>(
401
+ fn: LoaderFn<T, Record<string, string | undefined>, any>
402
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
403
+
404
+ // Overload 2: With function and fetchable flag
405
+ export function createLoader<T>(
406
+ fn: LoaderFn<T, Record<string, string | undefined>, any>,
407
+ fetchable: true
408
+ ): LoaderDefinition<Awaited<T>, Record<string, string | undefined>>;
409
+
410
+ // Implementation - function is ignored at runtime on client
411
+ // The $$id is injected by Vite plugin as hidden third parameter
412
+ export function createLoader(
413
+ _fn: LoaderFn<any, Record<string, string | undefined>, any>,
414
+ _fetchable?: true,
415
+ __injectedId?: string
416
+ ): LoaderDefinition<any, Record<string, string | undefined>> {
417
+ return {
418
+ __brand: "loader",
419
+ $$id: __injectedId || "",
420
+ };
421
+ }
422
+
423
+ /**
424
+ * Props for the ErrorBoundary component
425
+ */
426
+ export interface ErrorBoundaryProps {
427
+ /** Fallback UI to show when an error is caught */
428
+ fallback:
429
+ | ReactNode
430
+ | ((props: ClientErrorBoundaryFallbackProps) => ReactNode);
431
+ /** Children to render */
432
+ children: ReactNode;
433
+ /** Optional callback when an error is caught */
434
+ onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
435
+ }
436
+
437
+ interface ErrorBoundaryState {
438
+ hasError: boolean;
439
+ error: Error | null;
440
+ }
441
+
442
+ /**
443
+ * Client-side ErrorBoundary component
444
+ *
445
+ * Catches JavaScript errors in child components during rendering,
446
+ * in lifecycle methods, and in constructors of the whole tree below them.
447
+ * Displays a fallback UI instead of the component tree that crashed.
448
+ *
449
+ * Use this to wrap client components that might throw during hydration
450
+ * or user interaction. For server-side errors (middleware, loaders, handlers),
451
+ * use the errorBoundary() helper in route definitions instead.
452
+ *
453
+ * @example
454
+ * ```tsx
455
+ * "use client";
456
+ * import { ErrorBoundary } from "rsc-router/client";
457
+ *
458
+ * function MyComponent() {
459
+ * return (
460
+ * <ErrorBoundary fallback={<div>Something went wrong</div>}>
461
+ * <ComponentThatMightThrow />
462
+ * </ErrorBoundary>
463
+ * );
464
+ * }
465
+ *
466
+ * // Or with a function fallback for more control:
467
+ * function MyComponent() {
468
+ * return (
469
+ * <ErrorBoundary
470
+ * fallback={({ error, reset }) => (
471
+ * <div>
472
+ * <p>Error: {error.message}</p>
473
+ * <button onClick={reset}>Try again</button>
474
+ * </div>
475
+ * )}
476
+ * >
477
+ * <ComponentThatMightThrow />
478
+ * </ErrorBoundary>
479
+ * );
480
+ * }
481
+ * ```
482
+ */
483
+ export class ErrorBoundary extends Component<
484
+ ErrorBoundaryProps,
485
+ ErrorBoundaryState
486
+ > {
487
+ constructor(props: ErrorBoundaryProps) {
488
+ super(props);
489
+ this.state = { hasError: false, error: null };
490
+ }
491
+
492
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
493
+ return { hasError: true, error };
494
+ }
495
+
496
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
497
+ console.error("[ErrorBoundary] Error caught:", error, errorInfo);
498
+ this.props.onError?.(error, errorInfo);
499
+ }
500
+
501
+ reset = (): void => {
502
+ this.setState({ hasError: false, error: null });
503
+ };
504
+
505
+ render(): ReactNode {
506
+ if (this.state.hasError && this.state.error) {
507
+ const { fallback } = this.props;
508
+
509
+ // Create error info for the fallback
510
+ const errorInfo: ErrorInfo = {
511
+ message: this.state.error.message,
512
+ name: this.state.error.name,
513
+ stack: this.state.error.stack,
514
+ cause: this.state.error.cause,
515
+ segmentId: "client",
516
+ segmentType: "route",
517
+ };
518
+
519
+ // Render fallback - use createElement so hooks work in function fallbacks
520
+ if (typeof fallback === "function") {
521
+ return createElement(fallback, { error: errorInfo, reset: this.reset });
522
+ }
523
+
524
+ return fallback;
525
+ }
526
+
527
+ return this.props.children;
528
+ }
529
+ }
530
+
531
+ // ============================================================================
532
+ // Re-exports from browser/react for convenience
533
+ // These are the most commonly used client-side navigation utilities
534
+ // ============================================================================
535
+
536
+ // Navigation hooks
537
+ export {
538
+ useNavigation,
539
+ type NavigationMethods,
540
+ type NavigationValue,
541
+ } from "./browser/react/use-navigation.js";
542
+
543
+ // Action state tracking hook
544
+ export {
545
+ useAction,
546
+ type ServerActionFunction,
547
+ } from "./browser/react/use-action.js";
548
+
549
+ // Segments state hook
550
+ export {
551
+ useSegments,
552
+ type SegmentsState,
553
+ } from "./browser/react/use-segments.js";
554
+
555
+ // Client cache controls hook
556
+ export {
557
+ useClientCache,
558
+ type ClientCacheControls,
559
+ } from "./browser/react/use-client-cache.js";
560
+
561
+ // Provider
562
+ export {
563
+ NavigationProvider,
564
+ type NavigationProviderProps,
565
+ } from "./browser/react/NavigationProvider.js";
566
+
567
+ // Link component
568
+ export {
569
+ Link,
570
+ type LinkProps,
571
+ type PrefetchStrategy,
572
+ type StateOrGetter,
573
+ } from "./browser/react/Link.js";
574
+
575
+ // Link status hook
576
+ export {
577
+ useLinkStatus,
578
+ type LinkStatus,
579
+ } from "./browser/react/use-link-status.js";
580
+
581
+ // Scroll restoration
582
+ export {
583
+ ScrollRestoration,
584
+ useScrollRestoration,
585
+ type ScrollRestorationProps,
586
+ } from "./browser/react/ScrollRestoration.js";
587
+
588
+ // Handle API - for accumulating data across route segments
589
+ export { createHandle, isHandle, type Handle } from "./handle.js";
590
+
591
+ // Handle data hook
592
+ export { useHandle } from "./browser/react/use-handle.js";
593
+
594
+ // Built-in handles
595
+ export { Meta } from "./handles/meta.js";
596
+ export { MetaTags } from "./handles/MetaTags.js";
597
+ export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
598
+
599
+ // Location state - type-safe navigation state
600
+ export {
601
+ createLocationState,
602
+ useLocationState,
603
+ type LocationStateDefinition,
604
+ type LocationStateEntry,
605
+ } from "./browser/react/location-state.js";
606
+
607
+ // Type-safe href for client-side path validation
608
+ export { href, type ValidPaths, type PatternToPath, type PathResponse } from "./href-client.js";
609
+
610
+ // Response envelope types for consuming JSON response routes
611
+ export type { ResponseEnvelope, ResponseError } from "./urls.js";
612
+
613
+ /**
614
+ * Type guard for checking if a response envelope contains an error.
615
+ *
616
+ * @example
617
+ * ```typescript
618
+ * const result: ResponseEnvelope<Product> = await fetch(url).then(r => r.json());
619
+ * if (isResponseError(result)) {
620
+ * console.log(result.error.message, result.error.code);
621
+ * return;
622
+ * }
623
+ * result.data // fully typed as Product
624
+ * ```
625
+ */
626
+ export function isResponseError<T>(
627
+ result: import("./urls.js").ResponseEnvelope<T>
628
+ ): result is import("./urls.js").ResponseEnvelope<T> & { error: import("./urls.js").ResponseError } {
629
+ return result.error !== undefined;
630
+ }
631
+
632
+ // Mount context for include() scoped components
633
+ export { useMount } from "./browser/react/use-mount.js";
634
+ export { MountContext } from "./browser/react/mount-context.js";
635
+
636
+ // Mount-aware href hook - auto-prefixes paths with include() mount
637
+ export { useHref } from "./browser/react/use-href.js";
638
+
639
+ // Type-safe scoped reverse function for scopedReverse<typeof patterns>()
640
+ export type { ScopedReverseFunction } from "./reverse.js";
641
+
642
+ // Loader definition type - for typing loader props in client components
643
+ export type { LoaderDefinition } from "./types.js";
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Component utilities for RSC
3
+ *
4
+ * Helpers for working with React Server Components and client components.
5
+ */
6
+
7
+ import type { ComponentType } from "react";
8
+
9
+ /**
10
+ * Symbol used by React to mark client component references.
11
+ * When a file has "use client" directive, the bundler transforms the exports
12
+ * to include this symbol on $$typeof.
13
+ */
14
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
15
+
16
+ /**
17
+ * Check if a component is a client component (has "use client" directive).
18
+ *
19
+ * Client components are marked by the bundler with:
20
+ * - $$typeof: Symbol.for("react.client.reference")
21
+ * - $$id: string (module identifier)
22
+ *
23
+ * @param component - The component to check
24
+ * @returns true if the component has client reference marker
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { MyComponent } from "./my-component"; // has "use client"
29
+ *
30
+ * if (!isClientComponent(MyComponent)) {
31
+ * throw new Error("MyComponent must be a client component");
32
+ * }
33
+ * ```
34
+ */
35
+ export function isClientComponent(
36
+ component: ComponentType<unknown> | unknown
37
+ ): boolean {
38
+ if (typeof component !== "function") {
39
+ return false;
40
+ }
41
+ const withMeta = component as { $$typeof?: symbol };
42
+ return withMeta.$$typeof === CLIENT_REFERENCE;
43
+ }
44
+
45
+ /**
46
+ * Assert that a component is a client component.
47
+ * Throws a descriptive error if not.
48
+ *
49
+ * @param component - The component to check
50
+ * @param name - Name to use in error message (e.g., "document")
51
+ * @throws Error if the component is not a client component
52
+ */
53
+ export function assertClientComponent(
54
+ component: ComponentType<unknown> | unknown,
55
+ name: string
56
+ ): asserts component is ComponentType<unknown> {
57
+ if (typeof component !== "function") {
58
+ throw new Error(
59
+ `${name} must be a client component function with "use client" directive. ` +
60
+ `Make sure to pass the component itself, not a JSX element: ` +
61
+ `${name}: My${capitalize(name)} (correct) vs ${name}: <My${capitalize(name)} /> (incorrect)`
62
+ );
63
+ }
64
+
65
+ if (!isClientComponent(component)) {
66
+ throw new Error(
67
+ `${name} must be a client component with "use client" directive at the top of the file. ` +
68
+ `Server components cannot be used as the ${name} because their function reference ` +
69
+ `cannot be serialized in the RSC payload. Add "use client" to your ${name} file.`
70
+ );
71
+ }
72
+ }
73
+
74
+ function capitalize(str: string): string {
75
+ return str.charAt(0).toUpperCase() + str.slice(1);
76
+ }