@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -1,59 +1,69 @@
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
- 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 {
15
+ buildLoaderPromise,
16
+ getMemoizedLoaderPromise,
17
+ } from "./segment-loader-promise.js";
17
18
 
18
19
  // ViewTransition is only available in React experimental.
19
20
  // Access via namespace import to avoid compile-time errors on stable React.
20
21
  const ReactViewTransition: any =
21
22
  "ViewTransition" in React ? (React as any).ViewTransition : null;
22
23
 
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;
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
+
33
+ function restoreParallelLoaderMarkers(
34
+ segments: ResolvedSegment[],
35
+ ): ResolvedSegment[] {
36
+ const parallelLoadingByNamespace = new Map<string, ReactNode>();
37
+ let nextSegments: ResolvedSegment[] | null = null;
38
+
39
+ for (let i = 0; i < segments.length; i++) {
40
+ const segment = segments[i];
41
+
42
+ if (segment.type === "parallel") {
43
+ if (segment.namespace && isRenderableLoading(segment.loading)) {
44
+ parallelLoadingByNamespace.set(segment.namespace, segment.loading);
45
+ }
40
46
  continue;
41
47
  }
42
48
 
43
- if (result.ok) {
44
- loaderData[id] = result.data;
49
+ if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
45
50
  continue;
46
51
  }
47
52
 
48
- // Error case
49
- if (result.fallback) {
50
- errorFallback = result.fallback;
51
- } else {
52
- throw new Error(result.error.message);
53
+ const parallelLoading = segment.namespace
54
+ ? parallelLoadingByNamespace.get(segment.namespace)
55
+ : undefined;
56
+ if (parallelLoading === undefined) {
57
+ continue;
58
+ }
59
+
60
+ if (!nextSegments) {
61
+ nextSegments = segments.slice();
53
62
  }
63
+ nextSegments[i] = { ...segment, parallelLoading };
54
64
  }
55
65
 
56
- return { loaderData, errorFallback };
66
+ return nextSegments ?? segments;
57
67
  }
58
68
 
59
69
  /**
@@ -92,11 +102,61 @@ export interface RenderSegmentsOptions {
92
102
  rootLayout?: ComponentType<RootLayoutProps>;
93
103
  }
94
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
+
95
149
  /**
96
150
  * Render segments into a React tree with proper layout nesting
97
151
  *
98
- * Layouts nest using OutletProvider, while route + parallel + error + notFound segments
99
- * 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).
100
160
  *
101
161
  * Error segments are treated like route segments - they render their fallback
102
162
  * component in place of the failed segment. When an error occurs in a handler,
@@ -108,27 +168,30 @@ export interface RenderSegmentsOptions {
108
168
  * notFoundBoundary's fallback component.
109
169
  *
110
170
  * @param segments - Array of resolved segments to render
111
- * @returns ReactNode representing the component tree
171
+ * @returns Promise resolving to the ReactNode tree (the function is async)
112
172
  *
113
173
  * @example
114
174
  * ```typescript
115
175
  * const segments = [
116
- * { id: 'L0.0', type: 'layout', component: <RootLayout /> },
117
- * { id: 'L1.0', type: 'layout', component: <BlogLayout /> },
118
- * { id: 'R2.0', type: 'route', component: <BlogPost /> },
119
- * { 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' }
120
179
  * ];
121
180
  *
122
- * const tree = renderSegments(segments);
123
- * // Results in:
124
- * // <OutletProvider><RootLayout>
125
- * // <OutletProvider><BlogLayout>
126
- * // <><BlogPost /><Sidebar /></>
127
- * // </BlogLayout></OutletProvider>
128
- * // </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>
129
192
  *
130
193
  * // For server actions, pass isAction to await components:
131
- * const tree = renderSegments(segments, { isAction: true });
194
+ * const tree = await renderSegments(segments, { isAction: true });
132
195
  * ```
133
196
  */
134
197
  export async function renderSegments(
@@ -143,6 +206,10 @@ export async function renderSegments(
143
206
  } = options || {};
144
207
 
145
208
  const temporalLazyRefs: Promise<any>[] = [];
209
+ const normalizedSegments = restoreParallelLoaderMarkers(segments);
210
+ const normalizedInterceptSegments = interceptSegments
211
+ ? restoreParallelLoaderMarkers(interceptSegments)
212
+ : undefined;
146
213
 
147
214
  /**
148
215
  * Registers promises from lazy/async components for awaiting.
@@ -167,7 +234,26 @@ export async function renderSegments(
167
234
  );
168
235
  }
169
236
  // Separate segments by type, passing intercept segments for explicit injection
170
- const tree = segmentTreeWalk(segments, interceptSegments);
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
+ );
171
257
  // Render content segments as siblings
172
258
  let content: ReactNode = null;
173
259
  for (const node of tree) {
@@ -180,17 +266,31 @@ export async function renderSegments(
180
266
  );
181
267
  const { component, id, params, loading } = node.segment;
182
268
 
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
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.
189
288
  const includeParams =
190
- node.segment.type === "route" ||
191
289
  node.segment.type === "error" ||
192
290
  node.segment.type === "notFound" ||
193
- (node.segment.type === "layout" && node.segment.belongsToRoute);
291
+ ((node.segment.type === "route" ||
292
+ (node.segment.type === "layout" && node.segment.belongsToRoute)) &&
293
+ !inTransitionScope);
194
294
 
195
295
  const paramStr =
196
296
  includeParams && params && Object.keys(params).length > 0
@@ -199,69 +299,70 @@ export async function renderSegments(
199
299
  .map(([k, v]) => `${k}=${v}`)
200
300
  .join(",")
201
301
  : "";
202
- const key = `${paramStr ? `${id}-${paramStr}` : id}`;
302
+ const key = paramStr ? `${id}-${paramStr}` : id;
203
303
 
204
- // Get loader entries for this node
205
304
  const loaderEntries = node.loaders.filter(
206
305
  (loader) => loader.loaderId && loader.loaderData !== undefined,
207
306
  );
208
307
 
209
- // Determine the component content (with or without Suspense wrapper)
210
- // Wrap when loading skeleton defined OR component is Promise (needs Suspense)
211
- // During actions, await component Promise to prevent Suspense from triggering
212
- // This keeps existing content visible instead of showing loading skeleton
213
308
  let resolvedComponent = component;
214
309
  if (isAction && component instanceof Promise) {
215
310
  resolvedComponent = await component;
216
311
  }
217
312
 
218
- let nodeContent: ReactNode =
219
- loading !== null && loading !== undefined && loading !== false
220
- ? createElement(RouteContentWrapper, {
221
- key: `suspense-loading-${id}`,
222
- content:
223
- resolvedComponent instanceof Promise
224
- ? resolvedComponent
225
- : Promise.resolve(resolvedComponent),
226
- fallback: loading,
227
- segmentId: id,
228
- })
229
- : 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);
230
321
 
231
322
  // Wrap with <ViewTransition> if transition config exists (React experimental only).
232
323
  // An empty config ({}) creates a bare <ViewTransition> boundary that participates
233
324
  // in transitions without adding custom animation classes. Named element-level
234
325
  // <ViewTransition> components inside (with name/share props) morph independently
235
326
  // 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 =
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 =
245
345
  node.segment.type === "layout" ? content : null;
246
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
+
247
361
  // Prepare loader data if there are loaders
248
362
  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
-
260
- // Use LoaderBoundary when loading is defined to maintain consistent tree structure
261
- // This ensures cached segments (which may not have loader segments) have the same
262
- // tree structure as fresh segments, preventing React remounts
263
- // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
363
+
264
364
  if (loading !== undefined && loading !== null) {
365
+ const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
265
366
  content = createElement(LoaderBoundary, {
266
367
  key: `loader-boundary-${key}`,
267
368
  loaderDataPromise:
@@ -275,7 +376,6 @@ export async function renderSegments(
275
376
  children: nodeContent,
276
377
  });
277
378
  } else if (loaderEntries.length === 0) {
278
- // No loaders, no loading - simple OutletProvider
279
379
  content = createElement(OutletProvider, {
280
380
  key,
281
381
  content: outletContent,
@@ -284,13 +384,52 @@ export async function renderSegments(
284
384
  children: nodeContent,
285
385
  });
286
386
  } else {
287
- // Has loaders but no loading skeleton - await loaders and render directly
288
- const resolvedData = await loaderDataPromise;
289
- const { loaderData, errorFallback } = resolveLoaderData(
387
+ const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
388
+ const parallelOwnedLoaders = loaderEntries.filter(
389
+ (l) => !!l.parallelLoading,
390
+ );
391
+
392
+ const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
393
+ const resolvedData = await buildLoaderPromise(layoutLoaders);
394
+ const { loaderData, errorFallback } = decodeLoaderResults(
290
395
  resolvedData,
291
- loaderIds,
396
+ layoutLoaderIds,
292
397
  );
293
398
 
399
+ if (parallelOwnedLoaders.length > 0) {
400
+ const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
401
+
402
+ for (const loader of parallelOwnedLoaders) {
403
+ if (!loader.namespace) {
404
+ continue;
405
+ }
406
+ const existing = loadersByParallelNamespace.get(loader.namespace);
407
+ if (existing) {
408
+ existing.push(loader);
409
+ } else {
410
+ loadersByParallelNamespace.set(loader.namespace, [loader]);
411
+ }
412
+ }
413
+
414
+ for (const p of node.parallel) {
415
+ if (!p.loading || !p.namespace) {
416
+ continue;
417
+ }
418
+
419
+ const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
420
+ if (!ownedLoaders || ownedLoaders.length === 0) {
421
+ continue;
422
+ }
423
+
424
+ p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
425
+ const aggregated = getMemoizedLoaderPromise(ownedLoaders);
426
+ p.loaderDataPromise =
427
+ (forceAwait || isAction) && aggregated instanceof Promise
428
+ ? await aggregated
429
+ : aggregated;
430
+ }
431
+ }
432
+
294
433
  content = createElement(OutletProvider, {
295
434
  key,
296
435
  content: outletContent,
@@ -313,8 +452,6 @@ export async function renderSegments(
313
452
  }
314
453
  }
315
454
 
316
- // Always wrap with root error boundary to prevent white screens
317
- // This catches any unhandled errors that bubble up from the segment tree
318
455
  const errorBoundaryWrapped = createElement(RootErrorBoundary, {
319
456
  children: content,
320
457
  });
@@ -322,11 +459,8 @@ export async function renderSegments(
322
459
  await Promise.allSettled(temporalLazyRefs);
323
460
  }
324
461
 
325
- // Build the final result, optionally wrapped with root layout
326
462
  let result: ReactNode = errorBoundaryWrapped;
327
463
 
328
- // If rootLayout is provided, wrap the error boundary with it
329
- // This ensures the app shell stays mounted even during errors (prevents FOUC)
330
464
  if (RootLayout) {
331
465
  result = createElement(RootLayout, {
332
466
  children: errorBoundaryWrapped,
@@ -364,6 +498,28 @@ export async function renderSegments(
364
498
  * @param segments - Main segments from the route tree
365
499
  * @param interceptSegments - Optional intercept segments to inject
366
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
+
367
523
  function* segmentTreeWalk(
368
524
  segments: ResolvedSegment[],
369
525
  interceptSegments?: ResolvedSegment[],
@@ -384,19 +540,12 @@ function* segmentTreeWalk(
384
540
  // Extract parent ID from parallel ID
385
541
  // Example: "L0R1L0.@sidebar" → "L0R1L0"
386
542
  const parentId = segment.id.split(".")[0];
387
- if (!parallelsByParent.has(parentId)) {
388
- parallelsByParent.set(parentId, []);
389
- }
390
- parallelsByParent.get(parentId)!.push(segment);
543
+ pushToGroup(parallelsByParent, parentId, segment);
391
544
  } else if (segment.type === "loader") {
392
545
  // Extract parent ID from loader ID
393
- // Example: "L0D0.cart" → "L0"
394
- // Loader ID format: {parentShortCode}D{index}.{loaderId}
395
- const parentId = segment.id.split("D")[0];
396
- if (!loadersByParent.has(parentId)) {
397
- loadersByParent.set(parentId, []);
398
- }
399
- 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);
400
549
  } else {
401
550
  // Layout, route, error, and notFound segments are all rendered in the tree
402
551
  // Error/notFound segments replace the failed segment with fallback UI
@@ -411,17 +560,11 @@ function* segmentTreeWalk(
411
560
  if (intercept.type === "parallel" && intercept.slot) {
412
561
  // Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
413
562
  const parentId = intercept.id.split(".")[0];
414
- if (!parallelsByParent.has(parentId)) {
415
- parallelsByParent.set(parentId, []);
416
- }
417
- parallelsByParent.get(parentId)!.push(intercept);
563
+ pushToGroup(parallelsByParent, parentId, intercept);
418
564
  } else if (intercept.type === "loader") {
419
- // Intercept loaders - extract parent from loader ID
420
- const parentId = intercept.id.split("D")[0];
421
- if (!loadersByParent.has(parentId)) {
422
- loadersByParent.set(parentId, []);
423
- }
424
- 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);
425
568
  }
426
569
  }
427
570
  }