@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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