@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bd6e11bc

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +716 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -2,58 +2,61 @@ import * as React from "react";
2
2
  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
- import type {
6
- ResolvedSegment,
7
- LoaderDataResult,
8
- RootLayoutProps,
9
- } from "./types.js";
10
- import { isLoaderDataResult } from "./types.js";
5
+ import type { ResolvedSegment, RootLayoutProps } from "./types.js";
6
+ import { decodeLoaderResults } from "./decode-loader-results.js";
11
7
  import { invariant } from "./errors.js";
12
8
  import {
13
9
  RouteContentWrapper,
14
10
  LoaderBoundary,
15
11
  } from "./route-content-wrapper.js";
16
12
  import { RootErrorBoundary } from "./root-error-boundary.js";
13
+ import { getMemoizedContentPromise } from "./segment-content-promise.js";
14
+ import { getMemoizedLoaderPromise } from "./segment-loader-promise.js";
17
15
 
18
16
  // ViewTransition is only available in React experimental.
19
17
  // Access via namespace import to avoid compile-time errors on stable React.
20
18
  const ReactViewTransition: any =
21
19
  "ViewTransition" in React ? (React as any).ViewTransition : null;
22
20
 
23
- /**
24
- * Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
25
- */
26
- function resolveLoaderData(
27
- resolvedData: any[],
28
- loaderIds: string[],
29
- ): { loaderData: Record<string, any>; errorFallback: ReactNode } {
30
- const loaderData: Record<string, any> = {};
31
- let errorFallback: ReactNode = null;
32
-
33
- for (let i = 0; i < loaderIds.length; i++) {
34
- const id = loaderIds[i];
35
- const result = resolvedData[i];
36
-
37
- if (!isLoaderDataResult(result)) {
38
- // Legacy format - direct data
39
- loaderData[id] = result;
21
+ function restoreParallelLoaderMarkers(
22
+ segments: ResolvedSegment[],
23
+ ): ResolvedSegment[] {
24
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
25
+ let nextSegments: ResolvedSegment[] | null = null;
26
+
27
+ for (let i = 0; i < segments.length; i++) {
28
+ const segment = segments[i];
29
+
30
+ if (segment.type === "parallel") {
31
+ if (
32
+ segment.namespace &&
33
+ segment.loading !== undefined &&
34
+ segment.loading !== null &&
35
+ segment.loading !== false
36
+ ) {
37
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
38
+ }
40
39
  continue;
41
40
  }
42
41
 
43
- if (result.ok) {
44
- loaderData[id] = result.data;
42
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
45
43
  continue;
46
44
  }
47
45
 
48
- // Error case
49
- if (result.fallback) {
50
- errorFallback = result.fallback;
51
- } else {
52
- throw new Error(result.error.message);
46
+ const parallelLoading = segment.namespace
47
+ ? parallelLoadingByNamespace.get(segment.namespace)
48
+ : undefined;
49
+ if (parallelLoading === undefined) {
50
+ continue;
51
+ }
52
+
53
+ if (!nextSegments) {
54
+ nextSegments = segments.slice();
53
55
  }
56
+ nextSegments[i] = { ...segment, parallelLoading };
54
57
  }
55
58
 
56
- return { loaderData, errorFallback };
59
+ return nextSegments ?? segments;
57
60
  }
58
61
 
59
62
  /**
@@ -92,6 +95,50 @@ export interface RenderSegmentsOptions {
92
95
  rootLayout?: ComponentType<RootLayoutProps>;
93
96
  }
94
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
+
95
142
  /**
96
143
  * Render segments into a React tree with proper layout nesting
97
144
  *
@@ -143,6 +190,10 @@ export async function renderSegments(
143
190
  } = options || {};
144
191
 
145
192
  const temporalLazyRefs: Promise<any>[] = [];
193
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
194
+ const normalizedInterceptSegments = interceptSegments
195
+ ? restoreParallelLoaderMarkers(interceptSegments)
196
+ : undefined;
146
197
 
147
198
  /**
148
199
  * Registers promises from lazy/async components for awaiting.
@@ -167,7 +218,26 @@ export async function renderSegments(
167
218
  );
168
219
  }
169
220
  // Separate segments by type, passing intercept segments for explicit injection
170
- const tree = segmentTreeWalk(segments, interceptSegments);
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
+ );
171
241
  // Render content segments as siblings
172
242
  let content: ReactNode = null;
173
243
  for (const node of tree) {
@@ -180,17 +250,31 @@ export async function renderSegments(
180
250
  );
181
251
  const { component, id, params, loading } = node.segment;
182
252
 
183
- // Only include params in key for segments that belong to the route
184
- // - Routes: always include params (they render param-specific content)
185
- // - Error/notFound segments: always include params (they replace failed route content)
186
- // - Route's layouts (orphans): include params (children of parameterized route)
187
- // - Parent chain layouts: exclude params (shared across routes, param-agnostic)
188
- // 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.
189
272
  const includeParams =
190
- node.segment.type === "route" ||
191
273
  node.segment.type === "error" ||
192
274
  node.segment.type === "notFound" ||
193
- (node.segment.type === "layout" && node.segment.belongsToRoute);
275
+ ((node.segment.type === "route" ||
276
+ (node.segment.type === "layout" && node.segment.belongsToRoute)) &&
277
+ !inTransitionScope);
194
278
 
195
279
  const paramStr =
196
280
  includeParams && params && Object.keys(params).length > 0
@@ -219,10 +303,7 @@ export async function renderSegments(
219
303
  loading !== null && loading !== undefined && loading !== false
220
304
  ? createElement(RouteContentWrapper, {
221
305
  key: `suspense-loading-${id}`,
222
- content:
223
- resolvedComponent instanceof Promise
224
- ? resolvedComponent
225
- : Promise.resolve(resolvedComponent),
306
+ content: getMemoizedContentPromise(resolvedComponent),
226
307
  fallback: loading,
227
308
  segmentId: id,
228
309
  })
@@ -233,35 +314,51 @@ export async function renderSegments(
233
314
  // in transitions without adding custom animation classes. Named element-level
234
315
  // <ViewTransition> components inside (with name/share props) morph independently
235
316
  // from the parent's default cross-fade.
236
- if (ReactViewTransition && node.segment.transition) {
237
- nodeContent = createElement(ReactViewTransition, {
238
- ...node.segment.transition,
239
- children: nodeContent,
240
- });
241
- }
242
-
243
- // Common props for OutletProvider
244
- 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 =
245
335
  node.segment.type === "layout" ? content : null;
246
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
+
247
351
  // Prepare loader data if there are loaders
248
352
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
249
- const loaderDataPromise =
250
- loaderEntries.length > 0
251
- ? Promise.all(
252
- loaderEntries.map((loader) =>
253
- loader.loaderData instanceof Promise
254
- ? loader.loaderData
255
- : Promise.resolve(loader.loaderData),
256
- ),
257
- )
258
- : Promise.resolve([]);
259
353
 
260
354
  // Use LoaderBoundary when loading is defined to maintain consistent tree structure
261
355
  // This ensures cached segments (which may not have loader segments) have the same
262
356
  // tree structure as fresh segments, preventing React remounts
263
357
  // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
264
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);
265
362
  content = createElement(LoaderBoundary, {
266
363
  key: `loader-boundary-${key}`,
267
364
  loaderDataPromise:
@@ -284,13 +381,68 @@ export async function renderSegments(
284
381
  children: nodeContent,
285
382
  });
286
383
  } else {
287
- // Has loaders but no loading skeleton - await loaders and render directly
288
- const resolvedData = await loaderDataPromise;
289
- const { loaderData, errorFallback } = resolveLoaderData(
384
+ // Has loaders but no loading skeleton.
385
+ // Split: parallel-owned loaders stream (their parallel has loading()),
386
+ // layout-owned loaders are awaited (they gate the layout content).
387
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
388
+ const parallelOwnedLoaders = loaderEntries.filter(
389
+ (l) => !!l.parallelLoading,
390
+ );
391
+
392
+ // Await only layout-owned loaders
393
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
394
+ const layoutLoaderDataPromise =
395
+ layoutLoaders.length > 0
396
+ ? Promise.all(
397
+ layoutLoaders.map((l) =>
398
+ l.loaderData instanceof Promise
399
+ ? l.loaderData
400
+ : Promise.resolve(l.loaderData),
401
+ ),
402
+ )
403
+ : Promise.resolve([]);
404
+ const resolvedData = await layoutLoaderDataPromise;
405
+ const { loaderData, errorFallback } = decodeLoaderResults(
290
406
  resolvedData,
291
- loaderIds,
407
+ layoutLoaderIds,
292
408
  );
293
409
 
410
+ // Parallel-owned loaders: attach to their owning parallel segment
411
+ // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
412
+ if (parallelOwnedLoaders.length > 0) {
413
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
414
+
415
+ for (const loader of parallelOwnedLoaders) {
416
+ if (!loader.namespace) {
417
+ continue;
418
+ }
419
+ const existing = loadersByParallelNamespace.get(loader.namespace);
420
+ if (existing) {
421
+ existing.push(loader);
422
+ } else {
423
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
424
+ }
425
+ }
426
+
427
+ for (const p of node.parallel) {
428
+ if (!p.loading || !p.namespace) {
429
+ continue;
430
+ }
431
+
432
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
433
+ if (!ownedLoaders || ownedLoaders.length === 0) {
434
+ continue;
435
+ }
436
+
437
+ p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
438
+ const aggregated = getMemoizedLoaderPromise(ownedLoaders);
439
+ p.loaderDataPromise =
440
+ (forceAwait || isAction) && aggregated instanceof Promise
441
+ ? await aggregated
442
+ : aggregated;
443
+ }
444
+ }
445
+
294
446
  content = createElement(OutletProvider, {
295
447
  key,
296
448
  content: outletContent,
@@ -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
+ }