@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bdaf10aa

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