@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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