@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad

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