@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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