@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845

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 (252) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2138 -841
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +68 -21
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +3 -1
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +26 -4
  18. package/skills/layout/SKILL.md +6 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +15 -9
  22. package/skills/migrate-nextjs/SKILL.md +4 -2
  23. package/skills/migrate-react-router/SKILL.md +5 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +12 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -24
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +33 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +816 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +65 -9
  47. package/src/browser/navigation-client.ts +45 -25
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +52 -26
  51. package/src/browser/prefetch/cache.ts +124 -26
  52. package/src/browser/prefetch/fetch.ts +114 -38
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +18 -13
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-structure-assert.ts +2 -2
  71. package/src/browser/server-action-bridge.ts +23 -30
  72. package/src/browser/types.ts +21 -0
  73. package/src/build/collect-fallback-refs.ts +107 -0
  74. package/src/build/generate-manifest.ts +60 -35
  75. package/src/build/generate-route-types.ts +2 -0
  76. package/src/build/index.ts +2 -0
  77. package/src/build/route-trie.ts +2 -1
  78. package/src/build/route-types/codegen.ts +4 -4
  79. package/src/build/route-types/include-resolution.ts +1 -1
  80. package/src/build/route-types/per-module-writer.ts +7 -4
  81. package/src/build/route-types/router-processing.ts +55 -14
  82. package/src/build/route-types/scan-filter.ts +1 -1
  83. package/src/build/route-types/source-scan.ts +118 -0
  84. package/src/build/runtime-discovery.ts +9 -20
  85. package/src/cache/cache-scope.ts +28 -42
  86. package/src/cache/cf/cf-cache-store.ts +54 -13
  87. package/src/client.rsc.tsx +3 -0
  88. package/src/client.tsx +10 -8
  89. package/src/context-var.ts +5 -5
  90. package/src/decode-loader-results.ts +36 -0
  91. package/src/errors.ts +30 -1
  92. package/src/handle.ts +26 -13
  93. package/src/host/index.ts +2 -2
  94. package/src/host/router.ts +129 -57
  95. package/src/host/types.ts +31 -2
  96. package/src/host/utils.ts +1 -1
  97. package/src/href-client.ts +140 -20
  98. package/src/index.rsc.ts +9 -4
  99. package/src/index.ts +16 -6
  100. package/src/loader-store.ts +500 -0
  101. package/src/loader.rsc.ts +21 -6
  102. package/src/loader.ts +3 -10
  103. package/src/missing-id-error.ts +68 -0
  104. package/src/outlet-context.ts +1 -1
  105. package/src/prerender.ts +4 -4
  106. package/src/response-utils.ts +37 -0
  107. package/src/reverse.ts +65 -39
  108. package/src/route-content-wrapper.tsx +6 -28
  109. package/src/route-definition/dsl-helpers.ts +253 -265
  110. package/src/route-definition/helper-factories.ts +29 -139
  111. package/src/route-definition/helpers-types.ts +43 -15
  112. package/src/route-definition/resolve-handler-use.ts +6 -0
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/handler-context.ts +21 -41
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +3 -3
  121. package/src/router/loader-resolution.ts +19 -2
  122. package/src/router/match-api.ts +4 -3
  123. package/src/router/match-handlers.ts +63 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +53 -32
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +15 -26
  129. package/src/router/middleware.ts +99 -84
  130. package/src/router/pattern-matching.ts +101 -17
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +58 -2
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +27 -6
  139. package/src/router/segment-resolution/revalidation.ts +147 -106
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/substitute-pattern-params.ts +56 -0
  142. package/src/router/telemetry.ts +99 -0
  143. package/src/router/trie-matching.ts +18 -13
  144. package/src/router/types.ts +8 -0
  145. package/src/router/url-params.ts +49 -0
  146. package/src/router.ts +38 -23
  147. package/src/rsc/handler-context.ts +2 -2
  148. package/src/rsc/handler.ts +28 -69
  149. package/src/rsc/helpers.ts +91 -43
  150. package/src/rsc/index.ts +1 -1
  151. package/src/rsc/origin-guard.ts +28 -10
  152. package/src/rsc/progressive-enhancement.ts +4 -0
  153. package/src/rsc/response-route-handler.ts +46 -53
  154. package/src/rsc/rsc-rendering.ts +35 -51
  155. package/src/rsc/runtime-warnings.ts +9 -10
  156. package/src/rsc/server-action.ts +17 -37
  157. package/src/rsc/ssr-setup.ts +16 -0
  158. package/src/rsc/types.ts +8 -2
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +122 -56
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +118 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +20 -42
  165. package/src/ssr/index.tsx +5 -1
  166. package/src/static-handler.ts +1 -1
  167. package/src/testing/cache-status.ts +166 -0
  168. package/src/testing/collect-handle.ts +63 -0
  169. package/src/testing/dispatch.ts +440 -0
  170. package/src/testing/dom.entry.ts +22 -0
  171. package/src/testing/e2e/fixture.ts +154 -0
  172. package/src/testing/e2e/index.ts +149 -0
  173. package/src/testing/e2e/matchers.ts +51 -0
  174. package/src/testing/e2e/page-helpers.ts +272 -0
  175. package/src/testing/e2e/parity.ts +306 -0
  176. package/src/testing/e2e/server.ts +183 -0
  177. package/src/testing/flight-matchers.ts +104 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +332 -0
  180. package/src/testing/flight.entry.ts +46 -0
  181. package/src/testing/flight.ts +224 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +267 -0
  188. package/src/testing/render-route.tsx +565 -0
  189. package/src/testing/run-loader.ts +341 -0
  190. package/src/testing/run-middleware.ts +188 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +270 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/segments.ts +35 -1
  202. package/src/urls/include-helper.ts +10 -53
  203. package/src/urls/index.ts +0 -3
  204. package/src/urls/path-helper-types.ts +11 -3
  205. package/src/urls/path-helper.ts +17 -52
  206. package/src/urls/pattern-types.ts +36 -19
  207. package/src/urls/response-types.ts +22 -29
  208. package/src/urls/type-extraction.ts +26 -116
  209. package/src/urls/urls-function.ts +1 -5
  210. package/src/use-loader.tsx +413 -42
  211. package/src/vite/debug.ts +185 -0
  212. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  213. package/src/vite/discovery/discover-routers.ts +101 -51
  214. package/src/vite/discovery/discovery-errors.ts +194 -0
  215. package/src/vite/discovery/gate-state.ts +171 -0
  216. package/src/vite/discovery/prerender-collection.ts +67 -26
  217. package/src/vite/discovery/route-types-writer.ts +40 -84
  218. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  219. package/src/vite/discovery/state.ts +33 -0
  220. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  221. package/src/vite/index.ts +2 -0
  222. package/src/vite/plugin-types.ts +67 -0
  223. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  224. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  225. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  226. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  228. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  229. package/src/vite/plugins/expose-action-id.ts +54 -30
  230. package/src/vite/plugins/expose-id-utils.ts +12 -8
  231. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  232. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  233. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  234. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  235. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  236. package/src/vite/plugins/performance-tracks.ts +29 -25
  237. package/src/vite/plugins/use-cache-transform.ts +65 -50
  238. package/src/vite/plugins/version-injector.ts +39 -23
  239. package/src/vite/plugins/version-plugin.ts +59 -2
  240. package/src/vite/plugins/virtual-entries.ts +2 -2
  241. package/src/vite/rango.ts +116 -29
  242. package/src/vite/router-discovery.ts +750 -100
  243. package/src/vite/utils/ast-handler-extract.ts +15 -15
  244. package/src/vite/utils/banner.ts +1 -1
  245. package/src/vite/utils/bundle-analysis.ts +4 -2
  246. package/src/vite/utils/client-chunks.ts +190 -0
  247. package/src/vite/utils/forward-user-plugins.ts +193 -0
  248. package/src/vite/utils/manifest-utils.ts +21 -5
  249. package/src/vite/utils/package-resolution.ts +41 -1
  250. package/src/vite/utils/prerender-utils.ts +5 -4
  251. package/src/vite/utils/shared-utils.ts +107 -26
  252. package/src/browser/action-response-classifier.ts +0 -99
@@ -3,7 +3,7 @@ import { createElement, type ReactNode, type ComponentType } from "react";
3
3
  import { OutletProvider } from "./client.js";
4
4
  import { MountContextProvider } from "./browser/react/mount-context.js";
5
5
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
6
- import { isLoaderDataResult } from "./types.js";
6
+ import { decodeLoaderResults } from "./decode-loader-results.js";
7
7
  import { invariant } from "./errors.js";
8
8
  import {
9
9
  RouteContentWrapper,
@@ -59,42 +59,6 @@ function restoreParallelLoaderMarkers(
59
59
  return nextSegments ?? segments;
60
60
  }
61
61
 
62
- /**
63
- * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
64
- */
65
- function resolveLoaderData(
66
- resolvedData: any[],
67
- loaderIds: string[],
68
- ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
69
- const loaderData: Record<string, any> = {};
70
- let errorFallback: ReactNode = null;
71
-
72
- for (let i = 0; i < loaderIds.length; i++) {
73
- const id = loaderIds[i];
74
- const result = resolvedData[i];
75
-
76
- if (!isLoaderDataResult(result)) {
77
- // Legacy format - direct data
78
- loaderData[id] = result;
79
- continue;
80
- }
81
-
82
- if (result.ok) {
83
- loaderData[id] = result.data;
84
- continue;
85
- }
86
-
87
- // Error case
88
- if (result.fallback) {
89
- errorFallback = result.fallback;
90
- } else {
91
- throw new Error(result.error.message);
92
- }
93
- }
94
-
95
- return { loaderData, errorFallback };
96
- }
97
-
98
62
  /**
99
63
  * Options for renderSegments
100
64
  */
@@ -131,6 +95,50 @@ export interface RenderSegmentsOptions {
131
95
  rootLayout?: ComponentType<RootLayoutProps>;
132
96
  }
133
97
 
98
+ function createViewTransitionBoundary(
99
+ transition: NonNullable<ResolvedSegment["transition"]>,
100
+ children: ReactNode,
101
+ ): ReactNode {
102
+ // `viewTransition` is a router-specific flag (boundary opt-out), not a React
103
+ // <ViewTransition> prop — strip it so it never reaches React.
104
+ const { viewTransition: _viewTransition, ...vtProps } = transition;
105
+ return createElement(ReactViewTransition, {
106
+ ...vtProps,
107
+ children,
108
+ });
109
+ }
110
+
111
+ function wrapDefaultOutletContent(
112
+ content: ReactNode,
113
+ transition: NonNullable<ResolvedSegment["transition"]>,
114
+ ): ReactNode {
115
+ if (!React.isValidElement(content)) {
116
+ return createViewTransitionBoundary(transition, content);
117
+ }
118
+
119
+ const props = content.props as any;
120
+
121
+ if (content.type === MountContextProvider) {
122
+ return React.cloneElement(content, {
123
+ children: wrapDefaultOutletContent(props.children, transition),
124
+ } as any);
125
+ }
126
+
127
+ if (content.type === OutletProvider && props.segment?.type === "layout") {
128
+ return React.cloneElement(content, {
129
+ content: wrapDefaultOutletContent(props.content, transition),
130
+ } as any);
131
+ }
132
+
133
+ if (content.type === LoaderBoundary && props.segment?.type === "layout") {
134
+ return React.cloneElement(content, {
135
+ outletContent: wrapDefaultOutletContent(props.outletContent, transition),
136
+ } as any);
137
+ }
138
+
139
+ return createViewTransitionBoundary(transition, content);
140
+ }
141
+
134
142
  /**
135
143
  * Render segments into a React tree with proper layout nesting
136
144
  *
@@ -211,6 +219,25 @@ export async function renderSegments(
211
219
  }
212
220
  // Separate segments by type, passing intercept segments for explicit injection
213
221
  const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
222
+
223
+ // A route is "in a transition scope" when its own segment OR any layout in
224
+ // its matched chain declares transition(). Both transition() forms land here:
225
+ // the per-route item form sets transition on the route entry, and the block
226
+ // wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
227
+ // in scope, the route and its route-owned layouts use param-agnostic keys so a
228
+ // same-route navigation reconciles (holds content) instead of remounting. The
229
+ // value is a static property of the route's position in the tree, so it is the
230
+ // same on every render of that route (SSR, navigation, action) — the keys
231
+ // never drift. Cross-route navigation still remounts: different routes have
232
+ // different segment ids regardless of transition scope.
233
+ const inTransitionScope = normalizedSegments.some(
234
+ (s) =>
235
+ s.transition != null &&
236
+ (s.type === "layout" ||
237
+ s.type === "route" ||
238
+ s.type === "error" ||
239
+ s.type === "notFound"),
240
+ );
214
241
  // Render content segments as siblings
215
242
  let content: ReactNode = null;
216
243
  for (const node of tree) {
@@ -223,17 +250,31 @@ export async function renderSegments(
223
250
  );
224
251
  const { component, id, params, loading } = node.segment;
225
252
 
226
- // Only include params in key for segments that belong to the route
227
- // - Routes: always include params (they render param-specific content)
228
- // - Error/notFound segments: always include params (they replace failed route content)
229
- // - Route's layouts (orphans): include params (children of parameterized route)
230
- // - Parent chain layouts: exclude params (shared across routes, param-agnostic)
231
- // This prevents unnecessary unmounting when params change
253
+ // Param-agnostic keys are opt-in via the transition() DSL (see
254
+ // inTransitionScope above). A route (and its route-owned layouts) inside a
255
+ // transition scope drops the param from its key, so navigating between two
256
+ // param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
257
+ // the route subtree instead of remounting it. Combined with the
258
+ // startTransition wrap that shouldStartViewTransition already applies to
259
+ // transition routes (browser/partial-update.ts), the previous content stays
260
+ // on screen while the new loaders resolve (stale-while-revalidate) instead
261
+ // of flashing the loading skeleton. This works on stable React; experimental
262
+ // React adds the animated <ViewTransition> cross-fade on top.
263
+ //
264
+ // Outside a transition scope the key stays param-bearing and the route
265
+ // remounts on param change (the default: a fresh skeleton and fresh
266
+ // component state).
267
+ //
268
+ // error/notFound always keep param-bearing keys: createErrorSegment reuses
269
+ // the boundary layout's shortCode as the error segment id (router/
270
+ // error-handling.ts), so a param-agnostic error key could collide with that
271
+ // layout's key within the same render.
232
272
  const includeParams =
233
- node.segment.type === "route" ||
234
273
  node.segment.type === "error" ||
235
274
  node.segment.type === "notFound" ||
236
- (node.segment.type === "layout" && node.segment.belongsToRoute);
275
+ ((node.segment.type === "route" ||
276
+ (node.segment.type === "layout" && node.segment.belongsToRoute)) &&
277
+ !inTransitionScope);
237
278
 
238
279
  const paramStr =
239
280
  includeParams && params && Object.keys(params).length > 0
@@ -273,26 +314,51 @@ export async function renderSegments(
273
314
  // in transitions without adding custom animation classes. Named element-level
274
315
  // <ViewTransition> components inside (with name/share props) morph independently
275
316
  // from the parent's default cross-fade.
276
- if (ReactViewTransition && node.segment.transition) {
277
- nodeContent = createElement(ReactViewTransition, {
278
- ...node.segment.transition,
279
- children: nodeContent,
280
- });
281
- }
282
-
283
- // Common props for OutletProvider
284
- const outletContent: ReactNode =
317
+ //
318
+ // For layouts, wrap the outlet content (what `<Outlet />` renders) rather
319
+ // than the layout component itself. Parallel slots like `<ParallelOutlet
320
+ // name="@modal" />` read from a separate context channel and end up as
321
+ // siblings of the VT in the rendered tree, so modal mounts don't trigger a
322
+ // subtree update on the layout-level VT — which would otherwise make
323
+ // React's commit walker fire `document.startViewTransition` and apply
324
+ // view-transition-names to the underlying main subtree (cover/title/etc.).
325
+ //
326
+ // `transition.viewTransition === false` opts out of the router-owned
327
+ // boundary only. Driving (the startTransition wrap in browser/partial-update.ts
328
+ // and the param-agnostic key/hold below) keys off transition *presence*, not
329
+ // this flag, so a boundary-less transition still holds content and lets
330
+ // consumer-placed <ViewTransition> elements animate. The global
331
+ // createRouter({ viewTransition }) default is resolved into this field
332
+ // during segment resolution (only `false` is stamped; unset/"auto" is left
333
+ // as-is and means "wrap"), so this gate needs no router-option threading.
334
+ let outletContent: ReactNode =
285
335
  node.segment.type === "layout" ? content : null;
286
336
 
337
+ const transition = node.segment.transition;
338
+
339
+ if (
340
+ ReactViewTransition &&
341
+ transition &&
342
+ transition.viewTransition !== false
343
+ ) {
344
+ if (node.segment.type === "layout") {
345
+ outletContent = wrapDefaultOutletContent(outletContent, transition);
346
+ } else {
347
+ nodeContent = createViewTransitionBoundary(transition, nodeContent);
348
+ }
349
+ }
350
+
287
351
  // Prepare loader data if there are loaders
288
352
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
289
- const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
290
353
 
291
354
  // Use LoaderBoundary when loading is defined to maintain consistent tree structure
292
355
  // This ensures cached segments (which may not have loader segments) have the same
293
356
  // tree structure as fresh segments, preventing React remounts
294
357
  // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
295
358
  if (loading !== undefined && loading !== null) {
359
+ // Aggregate built here only — the loaderless and no-loading branches don't
360
+ // read it (the latter builds its own per-parallel promises).
361
+ const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
296
362
  content = createElement(LoaderBoundary, {
297
363
  key: `loader-boundary-${key}`,
298
364
  loaderDataPromise:
@@ -336,7 +402,7 @@ export async function renderSegments(
336
402
  )
337
403
  : Promise.resolve([]);
338
404
  const resolvedData = await layoutLoaderDataPromise;
339
- const { loaderData, errorFallback } = resolveLoaderData(
405
+ const { loaderData, errorFallback } = decodeLoaderResults(
340
406
  resolvedData,
341
407
  layoutLoaderIds,
342
408
  );
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Wire-type serialization transforms.
3
+ *
4
+ * The type a handler or loader returns on the server is frequently NOT the type
5
+ * a client receives after serialization. These transforms model that boundary so
6
+ * consumer-facing types (e.g. `Rango.PathResponse`) describe the wire value, not
7
+ * the source value.
8
+ *
9
+ * Two serializers, two transforms — they are intentionally NOT interchangeable:
10
+ *
11
+ * - `JsonSerialize<T>` models plain `JSON.stringify` (`path.json()` /
12
+ * `fetch().then(r => r.json())`). Lossy: `Date -> string`, `undefined` /
13
+ * functions / symbols dropped, `Map`/`Set` -> `{}`. `bigint` *throws* (no wire
14
+ * value), so it collapses the whole result to `never`. Honors `toJSON()`.
15
+ * - `FlightSerialize<T>` models React RSC Flight (loaders, RSC props, cache).
16
+ * High fidelity: `Date`/`Map`/`Set`/`bigint`/typed arrays/`Promise` are
17
+ * preserved; ordinary functions and non-global symbols do not cross.
18
+ *
19
+ * ## Overriding (full-transform replacement)
20
+ *
21
+ * Because `Rango.JsonSerialize` / `Rango.FlightSerialize` are type *aliases*, TS
22
+ * cannot let you redefine them directly (aliases don't merge). Instead each alias
23
+ * consults a generic override slot — augment it with a single member that is your
24
+ * complete transform. Delegate to the built-in for the cases you don't change:
25
+ *
26
+ * ```ts
27
+ * declare global {
28
+ * namespace Rango {
29
+ * interface FlightSerializeOverride<T> {
30
+ * app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
31
+ * }
32
+ * }
33
+ * }
34
+ * // now Rango.FlightSerialize<Money> is number; everything else is the built-in.
35
+ * ```
36
+ *
37
+ * Provide exactly one member: the slot is read as `Override<T>[keyof Override<T>]`,
38
+ * so multiple members union (and conflict). The built-in recurses through the
39
+ * override-aware alias, so an override applies at every nesting level.
40
+ */
41
+
42
+ import type { ReactNode } from "react";
43
+
44
+ type JsonPrimitive = string | number | boolean | null;
45
+
46
+ type AnyFunction = (...args: never[]) => unknown;
47
+
48
+ // --- JSON ---------------------------------------------------------------------
49
+
50
+ /**
51
+ * Internal marker for a value that makes `JSON.stringify` throw (`bigint`, or a
52
+ * `toJSON()` returning one). Distinct from `never`, which means "omitted":
53
+ * `undefined`/function/symbol-valued keys are dropped, and such array slots
54
+ * become `null`. A throwing value has no valid JSON wire form, so it propagates
55
+ * up through every container and is excluded at the public boundary (`bigint`
56
+ * alone -> `never`; `{ id: bigint }` -> `never`).
57
+ */
58
+ declare const JSON_THROWS: unique symbol;
59
+ type JsonThrows = typeof JSON_THROWS;
60
+
61
+ /** True if union `U` contains the throw marker. */
62
+ type HasThrow<U> = [Extract<U, JsonThrows>] extends [never] ? false : true;
63
+
64
+ /** Map a JSON array/tuple: propagate a throw; else omitted elements become null. */
65
+ type JsonSerializeArray<T extends readonly unknown[]> =
66
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[number]> extends true
67
+ ? JsonThrows
68
+ : {
69
+ [K in keyof T]: [JsonRawResolve<T[K]>] extends [never]
70
+ ? null
71
+ : JsonRawResolve<T[K]>;
72
+ };
73
+
74
+ /** Map a JSON object: propagate a throw; else drop omitted keys. */
75
+ type JsonSerializeObject<T> =
76
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[keyof T]> extends true
77
+ ? JsonThrows
78
+ : {
79
+ [K in keyof T as [JsonRawResolve<T[K]>] extends [never]
80
+ ? never
81
+ : K]: JsonRawResolve<T[K]>;
82
+ };
83
+
84
+ /**
85
+ * Built-in JSON rules, *raw* (may yield the throw marker). Honors `toJSON()` (so
86
+ * `Date -> string` and any class with `toJSON()` serialize correctly), preserves
87
+ * JSON primitives and literals, omits functions / symbols / `undefined`,
88
+ * collapses `Map`/`Set` to `{}`, and marks `bigint` as throwing. Recurses through
89
+ * the override-aware resolver, so registered overrides apply at every level.
90
+ */
91
+ type JsonSerializeBuiltinRaw<T> = T extends {
92
+ toJSON(...args: never[]): infer R;
93
+ }
94
+ ? JsonRawResolve<R>
95
+ : T extends JsonPrimitive
96
+ ? T
97
+ : T extends bigint
98
+ ? JsonThrows
99
+ : T extends AnyFunction
100
+ ? never
101
+ : T extends symbol
102
+ ? never
103
+ : T extends undefined
104
+ ? never
105
+ : T extends readonly unknown[]
106
+ ? JsonSerializeArray<T>
107
+ : T extends ReadonlyMap<unknown, unknown>
108
+ ? {}
109
+ : T extends ReadonlySet<unknown>
110
+ ? {}
111
+ : T extends object
112
+ ? JsonSerializeObject<T>
113
+ : never;
114
+
115
+ /** Override-aware raw JSON resolution (the recursion entry). */
116
+ type JsonRawResolve<T> = [keyof Rango.JsonSerializeOverride<T>] extends [never]
117
+ ? JsonSerializeBuiltinRaw<T>
118
+ : Rango.JsonSerializeOverride<T>[keyof Rango.JsonSerializeOverride<T>];
119
+
120
+ /**
121
+ * Model the result of round-tripping a value through `JSON.stringify` /
122
+ * `JSON.parse`. A registered `Rango.JsonSerializeOverride` replaces the transform
123
+ * wholesale; otherwise the built-in rules apply. Throwing values collapse to
124
+ * `never`.
125
+ */
126
+ export type JsonSerialize<T> = Exclude<JsonRawResolve<T>, JsonThrows>;
127
+
128
+ // --- Flight -------------------------------------------------------------------
129
+
130
+ /**
131
+ * Built-in Flight rules. Mirrors React's `ReactClientValue` contract: primitives
132
+ * including `bigint`, `undefined`, `null`, symbols, `Date`, `ArrayBuffer` and
133
+ * typed-array views, `Map`, `Set`, `FormData`, `Blob`, `Promise`,
134
+ * `ReadableStream`, and (async) iterables are preserved; ordinary functions
135
+ * resolve to `never`. JSX (`ReactNode`, and the async-node union
136
+ * `ReactNode | Promise<ReactNode>`) is preserved as-is via a non-distributive
137
+ * leaf, so handle/loader returns that carry JSX round-trip unchanged. Recurses
138
+ * through the override-aware `FlightSerialize`.
139
+ *
140
+ * The source of truth is React's own contract, which is intentionally NOT
141
+ * semver-stable across RSC framework APIs — this tracks the React version Rango
142
+ * pins. See:
143
+ * https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components
144
+ *
145
+ * Type-level limitations (not detectable structurally, so not modeled): class
146
+ * instances and null-prototype objects are rejected by React at runtime but pass
147
+ * here as their structural shape; non-global symbols are rejected at runtime but
148
+ * `symbol` is preserved here; Server Functions would need an override to be
149
+ * distinguished from ordinary functions (which resolve to `never`).
150
+ */
151
+ type FlightSerializeBuiltinRaw<T> = [T] extends [ReactNode | Promise<ReactNode>]
152
+ ? T
153
+ : T extends string | number | boolean | bigint | symbol | null | undefined
154
+ ? T
155
+ : T extends AnyFunction
156
+ ? never
157
+ : T extends Date
158
+ ? Date
159
+ : T extends ArrayBuffer
160
+ ? ArrayBuffer
161
+ : T extends ArrayBufferView
162
+ ? T
163
+ : T extends FormData
164
+ ? FormData
165
+ : T extends Blob
166
+ ? Blob
167
+ : T extends Map<infer K, infer V>
168
+ ? Map<FlightSerialize<K>, FlightSerialize<V>>
169
+ : T extends Set<infer V>
170
+ ? Set<FlightSerialize<V>>
171
+ : T extends Promise<infer V>
172
+ ? Promise<FlightSerialize<V>>
173
+ : T extends ReadableStream<infer V>
174
+ ? ReadableStream<FlightSerialize<V>>
175
+ : T extends readonly unknown[]
176
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
177
+ : T extends AsyncIterable<infer V>
178
+ ? AsyncIterable<FlightSerialize<V>>
179
+ : T extends Iterable<infer V>
180
+ ? Iterable<FlightSerialize<V>>
181
+ : T extends object
182
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
183
+ : never;
184
+
185
+ /**
186
+ * Model React RSC Flight serialization. A registered `Rango.FlightSerializeOverride`
187
+ * replaces the transform wholesale; otherwise the built-in rules apply.
188
+ */
189
+ export type FlightSerialize<T> = [
190
+ keyof Rango.FlightSerializeOverride<T>,
191
+ ] extends [never]
192
+ ? FlightSerializeBuiltinRaw<T>
193
+ : Rango.FlightSerializeOverride<T>[keyof Rango.FlightSerializeOverride<T>];
194
+
195
+ // Module-scoped aliases so the ambient `Rango.*` members below can reference the
196
+ // module-level transforms without the global namespace shadowing the names.
197
+ type GlobalJsonSerialize<T> = JsonSerialize<T>;
198
+ type GlobalJsonSerializeBuiltin<T> = JsonSerializeBuiltinRaw<T>;
199
+ type GlobalFlightSerialize<T> = FlightSerialize<T>;
200
+ type GlobalFlightSerializeBuiltin<T> = FlightSerializeBuiltinRaw<T>;
201
+
202
+ /**
203
+ * Ambient serialization transforms and their override slots on the `Rango`
204
+ * namespace. Available with no import wherever the router's types are in scope,
205
+ * alongside `Rango.Path` / `Rango.PathResponse`.
206
+ *
207
+ * `Rango.JsonSerialize` is what `Rango.PathResponse` applies; `Rango.FlightSerialize`
208
+ * is exposed for RSC/loader/cache wire types and must NOT be used for `path.json()`.
209
+ * `Rango.JsonSerializeBuiltin` / `Rango.FlightSerializeBuiltin` are the defaults,
210
+ * exported so an override can delegate to them.
211
+ */
212
+ declare global {
213
+ namespace Rango {
214
+ /**
215
+ * Full-transform override slot for `Rango.JsonSerialize`. Empty by default;
216
+ * augment with one member that is your complete transform (delegate to
217
+ * `Rango.JsonSerializeBuiltin<T>` for the cases you don't change).
218
+ */
219
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
220
+ interface JsonSerializeOverride<T> {}
221
+
222
+ /**
223
+ * Full-transform override slot for `Rango.FlightSerialize`. Empty by default;
224
+ * augment with one member that is your complete transform (delegate to
225
+ * `Rango.FlightSerializeBuiltin<T>` for the cases you don't change).
226
+ */
227
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
228
+ interface FlightSerializeOverride<T> {}
229
+
230
+ /** Wire type after `JSON.stringify` (`path.json()` / `fetch().json()`). */
231
+ type JsonSerialize<T> = GlobalJsonSerialize<T>;
232
+ /**
233
+ * Built-in `JsonSerialize` rules, for an override to delegate to. Raw: a
234
+ * `bigint`-bearing type yields the internal throw marker here, which
235
+ * `Rango.JsonSerialize` excludes to `never` at the boundary.
236
+ */
237
+ type JsonSerializeBuiltin<T> = GlobalJsonSerializeBuiltin<T>;
238
+ /** Wire type after RSC Flight serialization (loaders / RSC props / cache). */
239
+ type FlightSerialize<T> = GlobalFlightSerialize<T>;
240
+ /** Built-in `FlightSerialize` rules, for an override to delegate to. */
241
+ type FlightSerializeBuiltin<T> = GlobalFlightSerializeBuiltin<T>;
242
+ }
243
+ }