@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -1,9 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { createElement, type ReactNode, type ComponentType } from "react";
3
- import { OutletProvider } from "./client.js";
3
+ import { OutletProvider } from "./outlet-provider.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,
@@ -11,13 +11,25 @@ import {
11
11
  } from "./route-content-wrapper.js";
12
12
  import { RootErrorBoundary } from "./root-error-boundary.js";
13
13
  import { getMemoizedContentPromise } from "./segment-content-promise.js";
14
- import { getMemoizedLoaderPromise } from "./segment-loader-promise.js";
14
+ import {
15
+ buildLoaderPromise,
16
+ getMemoizedLoaderPromise,
17
+ } from "./segment-loader-promise.js";
15
18
 
16
19
  // ViewTransition is only available in React experimental.
17
20
  // Access via namespace import to avoid compile-time errors on stable React.
18
21
  const ReactViewTransition: any =
19
22
  "ViewTransition" in React ? (React as any).ViewTransition : null;
20
23
 
24
+ // A loading skeleton is renderable only when it is a real ReactNode value.
25
+ // `false` is treated as "not renderable" here. This is the three-term gate;
26
+ // the distinct two-term gate at the LoaderBoundary site deliberately treats
27
+ // `false` as "create a boundary without a RouteContentWrapper"
28
+ // (tree-structure.md), so it must NOT use this helper.
29
+ function isRenderableLoading(loading: ReactNode): boolean {
30
+ return loading !== undefined && loading !== null && loading !== false;
31
+ }
32
+
21
33
  function restoreParallelLoaderMarkers(
22
34
  segments: ResolvedSegment[],
23
35
  ): ResolvedSegment[] {
@@ -28,12 +40,7 @@ function restoreParallelLoaderMarkers(
28
40
  const segment = segments[i];
29
41
 
30
42
  if (segment.type === "parallel") {
31
- if (
32
- segment.namespace &&
33
- segment.loading !== undefined &&
34
- segment.loading !== null &&
35
- segment.loading !== false
36
- ) {
43
+ if (segment.namespace && isRenderableLoading(segment.loading)) {
37
44
  parallelLoadingByNamespace.set(segment.namespace, segment.loading);
38
45
  }
39
46
  continue;
@@ -59,42 +66,6 @@ function restoreParallelLoaderMarkers(
59
66
  return nextSegments ?? segments;
60
67
  }
61
68
 
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
69
  /**
99
70
  * Options for renderSegments
100
71
  */
@@ -131,11 +102,61 @@ export interface RenderSegmentsOptions {
131
102
  rootLayout?: ComponentType<RootLayoutProps>;
132
103
  }
133
104
 
105
+ function createViewTransitionBoundary(
106
+ transition: NonNullable<ResolvedSegment["transition"]>,
107
+ children: ReactNode,
108
+ ): ReactNode {
109
+ // `viewTransition` is a router-specific flag (boundary opt-out), not a React
110
+ // <ViewTransition> prop — strip it so it never reaches React.
111
+ const { viewTransition: _viewTransition, ...vtProps } = transition;
112
+ return createElement(ReactViewTransition, {
113
+ ...vtProps,
114
+ children,
115
+ });
116
+ }
117
+
118
+ function wrapDefaultOutletContent(
119
+ content: ReactNode,
120
+ transition: NonNullable<ResolvedSegment["transition"]>,
121
+ ): ReactNode {
122
+ if (!React.isValidElement(content)) {
123
+ return createViewTransitionBoundary(transition, content);
124
+ }
125
+
126
+ const props = content.props as any;
127
+
128
+ if (content.type === MountContextProvider) {
129
+ return React.cloneElement(content, {
130
+ children: wrapDefaultOutletContent(props.children, transition),
131
+ } as any);
132
+ }
133
+
134
+ if (content.type === OutletProvider && props.segment?.type === "layout") {
135
+ return React.cloneElement(content, {
136
+ content: wrapDefaultOutletContent(props.content, transition),
137
+ } as any);
138
+ }
139
+
140
+ if (content.type === LoaderBoundary && props.segment?.type === "layout") {
141
+ return React.cloneElement(content, {
142
+ outletContent: wrapDefaultOutletContent(props.outletContent, transition),
143
+ } as any);
144
+ }
145
+
146
+ return createViewTransitionBoundary(transition, content);
147
+ }
148
+
134
149
  /**
135
150
  * Render segments into a React tree with proper layout nesting
136
151
  *
137
- * Layouts nest using OutletProvider, while route + parallel + error + notFound segments
138
- * render as siblings in a Fragment.
152
+ * Layouts nest using OutletProvider; a layout receives the inner content via
153
+ * its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
154
+ * — they flow through OutletContext.parallel and are resolved where a layout
155
+ * places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
156
+ *
157
+ * The result is always wrapped in RootErrorBoundary so unhandled errors never
158
+ * blank the screen. When `options.rootLayout` is provided it wraps the error
159
+ * boundary at the OUTERMOST level (so the app shell survives errors).
139
160
  *
140
161
  * Error segments are treated like route segments - they render their fallback
141
162
  * component in place of the failed segment. When an error occurs in a handler,
@@ -147,27 +168,30 @@ export interface RenderSegmentsOptions {
147
168
  * notFoundBoundary's fallback component.
148
169
  *
149
170
  * @param segments - Array of resolved segments to render
150
- * @returns ReactNode representing the component tree
171
+ * @returns Promise resolving to the ReactNode tree (the function is async)
151
172
  *
152
173
  * @example
153
174
  * ```typescript
154
175
  * const segments = [
155
- * { id: 'L0.0', type: 'layout', component: <RootLayout /> },
156
- * { id: 'L1.0', type: 'layout', component: <BlogLayout /> },
157
- * { id: 'R2.0', type: 'route', component: <BlogPost /> },
158
- * { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
176
+ * { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
177
+ * { id: 'L0R1', type: 'route', component: <BlogPost /> },
178
+ * { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
159
179
  * ];
160
180
  *
161
- * const tree = renderSegments(segments);
162
- * // Results in:
163
- * // <OutletProvider><RootLayout>
164
- * // <OutletProvider><BlogLayout>
165
- * // <><BlogPost /><Sidebar /></>
166
- * // </BlogLayout></OutletProvider>
167
- * // </RootLayout></OutletProvider>
181
+ * // BlogLayout renders <Outlet /> for the route and
182
+ * // <ParallelOutlet name="@sidebar" /> for the parallel slot.
183
+ * const tree = await renderSegments(segments, { rootLayout: RootLayout });
184
+ * // Results in (outermost first):
185
+ * // <RootLayout>
186
+ * // <RootErrorBoundary>
187
+ * // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
188
+ * // <BlogPost />
189
+ * // </OutletProvider>
190
+ * // </RootErrorBoundary>
191
+ * // </RootLayout>
168
192
  *
169
193
  * // For server actions, pass isAction to await components:
170
- * const tree = renderSegments(segments, { isAction: true });
194
+ * const tree = await renderSegments(segments, { isAction: true });
171
195
  * ```
172
196
  */
173
197
  export async function renderSegments(
@@ -211,6 +235,25 @@ export async function renderSegments(
211
235
  }
212
236
  // Separate segments by type, passing intercept segments for explicit injection
213
237
  const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
238
+
239
+ // A route is "in a transition scope" when its own segment OR any layout in
240
+ // its matched chain declares transition(). Both transition() forms land here:
241
+ // the per-route item form sets transition on the route entry, and the block
242
+ // wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
243
+ // in scope, the route and its route-owned layouts use param-agnostic keys so a
244
+ // same-route navigation reconciles (holds content) instead of remounting. The
245
+ // value is a static property of the route's position in the tree, so it is the
246
+ // same on every render of that route (SSR, navigation, action) — the keys
247
+ // never drift. Cross-route navigation still remounts: different routes have
248
+ // different segment ids regardless of transition scope.
249
+ const inTransitionScope = normalizedSegments.some(
250
+ (s) =>
251
+ s.transition != null &&
252
+ (s.type === "layout" ||
253
+ s.type === "route" ||
254
+ s.type === "error" ||
255
+ s.type === "notFound"),
256
+ );
214
257
  // Render content segments as siblings
215
258
  let content: ReactNode = null;
216
259
  for (const node of tree) {
@@ -223,17 +266,31 @@ export async function renderSegments(
223
266
  );
224
267
  const { component, id, params, loading } = node.segment;
225
268
 
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
269
+ // Param-agnostic keys are opt-in via the transition() DSL (see
270
+ // inTransitionScope above). A route (and its route-owned layouts) inside a
271
+ // transition scope drops the param from its key, so navigating between two
272
+ // param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
273
+ // the route subtree instead of remounting it. Combined with the
274
+ // startTransition wrap that shouldStartViewTransition already applies to
275
+ // transition routes (browser/partial-update.ts), the previous content stays
276
+ // on screen while the new loaders resolve (stale-while-revalidate) instead
277
+ // of flashing the loading skeleton. This works on stable React; experimental
278
+ // React adds the animated <ViewTransition> cross-fade on top.
279
+ //
280
+ // Outside a transition scope the key stays param-bearing and the route
281
+ // remounts on param change (the default: a fresh skeleton and fresh
282
+ // component state).
283
+ //
284
+ // error/notFound always keep param-bearing keys: createErrorSegment reuses
285
+ // the boundary layout's shortCode as the error segment id (router/
286
+ // error-handling.ts), so a param-agnostic error key could collide with that
287
+ // layout's key within the same render.
232
288
  const includeParams =
233
- node.segment.type === "route" ||
234
289
  node.segment.type === "error" ||
235
290
  node.segment.type === "notFound" ||
236
- (node.segment.type === "layout" && node.segment.belongsToRoute);
291
+ ((node.segment.type === "route" ||
292
+ (node.segment.type === "layout" && node.segment.belongsToRoute)) &&
293
+ !inTransitionScope);
237
294
 
238
295
  const paramStr =
239
296
  includeParams && params && Object.keys(params).length > 0
@@ -242,57 +299,70 @@ export async function renderSegments(
242
299
  .map(([k, v]) => `${k}=${v}`)
243
300
  .join(",")
244
301
  : "";
245
- const key = `${paramStr ? `${id}-${paramStr}` : id}`;
302
+ const key = paramStr ? `${id}-${paramStr}` : id;
246
303
 
247
- // Get loader entries for this node
248
304
  const loaderEntries = node.loaders.filter(
249
305
  (loader) => loader.loaderId && loader.loaderData !== undefined,
250
306
  );
251
307
 
252
- // Determine the component content (with or without Suspense wrapper)
253
- // Wrap when loading skeleton defined OR component is Promise (needs Suspense)
254
- // During actions, await component Promise to prevent Suspense from triggering
255
- // This keeps existing content visible instead of showing loading skeleton
256
308
  let resolvedComponent = component;
257
309
  if (isAction && component instanceof Promise) {
258
310
  resolvedComponent = await component;
259
311
  }
260
312
 
261
- let nodeContent: ReactNode =
262
- loading !== null && loading !== undefined && loading !== false
263
- ? createElement(RouteContentWrapper, {
264
- key: `suspense-loading-${id}`,
265
- content: getMemoizedContentPromise(resolvedComponent),
266
- fallback: loading,
267
- segmentId: id,
268
- })
269
- : registerLazyRef(resolvedComponent);
313
+ let nodeContent: ReactNode = isRenderableLoading(loading)
314
+ ? createElement(RouteContentWrapper, {
315
+ key: `suspense-loading-${id}`,
316
+ content: getMemoizedContentPromise(resolvedComponent),
317
+ fallback: loading,
318
+ segmentId: id,
319
+ })
320
+ : registerLazyRef(resolvedComponent);
270
321
 
271
322
  // Wrap with <ViewTransition> if transition config exists (React experimental only).
272
323
  // An empty config ({}) creates a bare <ViewTransition> boundary that participates
273
324
  // in transitions without adding custom animation classes. Named element-level
274
325
  // <ViewTransition> components inside (with name/share props) morph independently
275
326
  // 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 =
327
+ //
328
+ // For layouts, wrap the outlet content (what `<Outlet />` renders) rather
329
+ // than the layout component itself. Parallel slots like `<ParallelOutlet
330
+ // name="@modal" />` read from a separate context channel and end up as
331
+ // siblings of the VT in the rendered tree, so modal mounts don't trigger a
332
+ // subtree update on the layout-level VT — which would otherwise make
333
+ // React's commit walker fire `document.startViewTransition` and apply
334
+ // view-transition-names to the underlying main subtree (cover/title/etc.).
335
+ //
336
+ // `transition.viewTransition === false` opts out of the router-owned
337
+ // boundary only. Driving (the startTransition wrap in browser/partial-update.ts
338
+ // and the param-agnostic key/hold below) keys off transition *presence*, not
339
+ // this flag, so a boundary-less transition still holds content and lets
340
+ // consumer-placed <ViewTransition> elements animate. The global
341
+ // createRouter({ viewTransition }) default is resolved into this field
342
+ // during segment resolution (only `false` is stamped; unset/"auto" is left
343
+ // as-is and means "wrap"), so this gate needs no router-option threading.
344
+ let outletContent: ReactNode =
285
345
  node.segment.type === "layout" ? content : null;
286
346
 
347
+ const transition = node.segment.transition;
348
+
349
+ if (
350
+ ReactViewTransition &&
351
+ transition &&
352
+ transition.viewTransition !== false
353
+ ) {
354
+ if (node.segment.type === "layout") {
355
+ outletContent = wrapDefaultOutletContent(outletContent, transition);
356
+ } else {
357
+ nodeContent = createViewTransitionBoundary(transition, nodeContent);
358
+ }
359
+ }
360
+
287
361
  // Prepare loader data if there are loaders
288
362
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
289
- const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
290
363
 
291
- // Use LoaderBoundary when loading is defined to maintain consistent tree structure
292
- // This ensures cached segments (which may not have loader segments) have the same
293
- // tree structure as fresh segments, preventing React remounts
294
- // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
295
364
  if (loading !== undefined && loading !== null) {
365
+ const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
296
366
  content = createElement(LoaderBoundary, {
297
367
  key: `loader-boundary-${key}`,
298
368
  loaderDataPromise:
@@ -306,7 +376,6 @@ export async function renderSegments(
306
376
  children: nodeContent,
307
377
  });
308
378
  } else if (loaderEntries.length === 0) {
309
- // No loaders, no loading - simple OutletProvider
310
379
  content = createElement(OutletProvider, {
311
380
  key,
312
381
  content: outletContent,
@@ -315,34 +384,18 @@ export async function renderSegments(
315
384
  children: nodeContent,
316
385
  });
317
386
  } else {
318
- // Has loaders but no loading skeleton.
319
- // Split: parallel-owned loaders stream (their parallel has loading()),
320
- // layout-owned loaders are awaited (they gate the layout content).
321
387
  const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
322
388
  const parallelOwnedLoaders = loaderEntries.filter(
323
389
  (l) => !!l.parallelLoading,
324
390
  );
325
391
 
326
- // Await only layout-owned loaders
327
392
  const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
328
- const layoutLoaderDataPromise =
329
- layoutLoaders.length > 0
330
- ? Promise.all(
331
- layoutLoaders.map((l) =>
332
- l.loaderData instanceof Promise
333
- ? l.loaderData
334
- : Promise.resolve(l.loaderData),
335
- ),
336
- )
337
- : Promise.resolve([]);
338
- const resolvedData = await layoutLoaderDataPromise;
339
- const { loaderData, errorFallback } = resolveLoaderData(
393
+ const resolvedData = await buildLoaderPromise(layoutLoaders);
394
+ const { loaderData, errorFallback } = decodeLoaderResults(
340
395
  resolvedData,
341
396
  layoutLoaderIds,
342
397
  );
343
398
 
344
- // Parallel-owned loaders: attach to their owning parallel segment
345
- // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
346
399
  if (parallelOwnedLoaders.length > 0) {
347
400
  const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
348
401
 
@@ -399,8 +452,6 @@ export async function renderSegments(
399
452
  }
400
453
  }
401
454
 
402
- // Always wrap with root error boundary to prevent white screens
403
- // This catches any unhandled errors that bubble up from the segment tree
404
455
  const errorBoundaryWrapped = createElement(RootErrorBoundary, {
405
456
  children: content,
406
457
  });
@@ -408,11 +459,8 @@ export async function renderSegments(
408
459
  await Promise.allSettled(temporalLazyRefs);
409
460
  }
410
461
 
411
- // Build the final result, optionally wrapped with root layout
412
462
  let result: ReactNode = errorBoundaryWrapped;
413
463
 
414
- // If rootLayout is provided, wrap the error boundary with it
415
- // This ensures the app shell stays mounted even during errors (prevents FOUC)
416
464
  if (RootLayout) {
417
465
  result = createElement(RootLayout, {
418
466
  children: errorBoundaryWrapped,
@@ -450,6 +498,28 @@ export async function renderSegments(
450
498
  * @param segments - Main segments from the route tree
451
499
  * @param interceptSegments - Optional intercept segments to inject
452
500
  */
501
+ // Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
502
+ // parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
503
+ // loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
504
+ // slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
505
+ // (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
506
+ // is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
507
+ // dropped the loader's data.
508
+ function loaderParentId(loaderSegmentId: string): string {
509
+ return loaderSegmentId.replace(/D\d+\..*$/, "");
510
+ }
511
+
512
+ // Append a value to the array stored under `key`, creating the array on first
513
+ // use. Single Map lookup (vs the has/get!().push double-lookup idiom).
514
+ function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
515
+ const arr = map.get(key);
516
+ if (arr) {
517
+ arr.push(value);
518
+ } else {
519
+ map.set(key, [value]);
520
+ }
521
+ }
522
+
453
523
  function* segmentTreeWalk(
454
524
  segments: ResolvedSegment[],
455
525
  interceptSegments?: ResolvedSegment[],
@@ -470,19 +540,12 @@ function* segmentTreeWalk(
470
540
  // Extract parent ID from parallel ID
471
541
  // Example: "L0R1L0.@sidebar" → "L0R1L0"
472
542
  const parentId = segment.id.split(".")[0];
473
- if (!parallelsByParent.has(parentId)) {
474
- parallelsByParent.set(parentId, []);
475
- }
476
- parallelsByParent.get(parentId)!.push(segment);
543
+ pushToGroup(parallelsByParent, parentId, segment);
477
544
  } else if (segment.type === "loader") {
478
545
  // Extract parent ID from loader ID
479
- // Example: "L0D0.cart" → "L0"
480
- // Loader ID format: {parentShortCode}D{index}.{loaderId}
481
- const parentId = segment.id.split("D")[0];
482
- if (!loadersByParent.has(parentId)) {
483
- loadersByParent.set(parentId, []);
484
- }
485
- loadersByParent.get(parentId)!.push(segment);
546
+ // Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
547
+ const parentId = loaderParentId(segment.id);
548
+ pushToGroup(loadersByParent, parentId, segment);
486
549
  } else {
487
550
  // Layout, route, error, and notFound segments are all rendered in the tree
488
551
  // Error/notFound segments replace the failed segment with fallback UI
@@ -497,17 +560,11 @@ function* segmentTreeWalk(
497
560
  if (intercept.type === "parallel" && intercept.slot) {
498
561
  // Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
499
562
  const parentId = intercept.id.split(".")[0];
500
- if (!parallelsByParent.has(parentId)) {
501
- parallelsByParent.set(parentId, []);
502
- }
503
- parallelsByParent.get(parentId)!.push(intercept);
563
+ pushToGroup(parallelsByParent, parentId, intercept);
504
564
  } else if (intercept.type === "loader") {
505
- // Intercept loaders - extract parent from loader ID
506
- const parentId = intercept.id.split("D")[0];
507
- if (!loadersByParent.has(parentId)) {
508
- loadersByParent.set(parentId, []);
509
- }
510
- loadersByParent.get(parentId)!.push(intercept);
565
+ // Intercept loaders - extract parent from loader ID (slot name preserved)
566
+ const parentId = loaderParentId(intercept.id);
567
+ pushToGroup(loadersByParent, parentId, intercept);
511
568
  }
512
569
  }
513
570
  }