@rangojs/router 0.0.0-experimental.bd6e11bc → 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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -11,6 +11,7 @@ import type { UrlPatterns } from "../urls.js";
11
11
  import type { UrlBuilder } from "../urls/pattern-types.js";
12
12
  import type { NamedRouteEntry } from "./content-negotiation.js";
13
13
  import type { TelemetrySink } from "./telemetry.js";
14
+ import type { RouterTracingConfig } from "./tracing.js";
14
15
  import type { RouterTimeouts, OnTimeoutCallback } from "./timeout.js";
15
16
 
16
17
  /**
@@ -496,6 +497,51 @@ export interface RangoOptions<TEnv = any> {
496
497
  */
497
498
  prefetchCacheTTL?: number | false;
498
499
 
500
+ /**
501
+ * Maximum number of decoded prefetch payloads the client keeps in its
502
+ * in-memory prefetch cache. When the cache is full the oldest entry is
503
+ * evicted (FIFO) to make room for a new prefetch.
504
+ *
505
+ * Each entry retains a fully decoded RSC payload (and the route's client
506
+ * chunks pulled in while decoding), so this is the lever on client-side
507
+ * prefetch memory: a higher value warms more routes at the cost of more
508
+ * retained payloads. Staleness is bounded separately by `prefetchCacheTTL`;
509
+ * this bounds the entry COUNT.
510
+ *
511
+ * Values below 1 (or non-finite) fall back to the default. To turn
512
+ * prefetching off entirely, set `prefetchCacheTTL: false` instead.
513
+ *
514
+ * @default 100
515
+ */
516
+ prefetchCacheSize?: number;
517
+
518
+ /**
519
+ * Maximum number of speculative prefetch requests (viewport/render strategy)
520
+ * the client runs concurrently. Hover prefetches bypass this queue and fire
521
+ * immediately; this caps only the background, idle-gated queue so prefetches
522
+ * never saturate the browser's connection pool.
523
+ *
524
+ * Values below 1 (or non-finite) fall back to the default.
525
+ *
526
+ * @default 2
527
+ */
528
+ prefetchConcurrency?: number;
529
+
530
+ /**
531
+ * Prefix for the rango state cookie name. The resolved name is
532
+ * `{prefix}_{routerId}`; the prefix is sanitized to cookie-name-safe
533
+ * characters (`[A-Za-z0-9-]`) and an empty result falls back to the default.
534
+ *
535
+ * The rango state cookie keys the client's prefetch / HTTP caches. Overriding
536
+ * the prefix lets you align it with cookie-naming policies or consent-manager
537
+ * classification lists, or avoid colliding with an existing `rango-state`
538
+ * cookie. It is not a full-name override: the `_{routerId}` suffix is what
539
+ * keeps sibling apps on one origin from clobbering each other's state.
540
+ *
541
+ * @default "rango-state"
542
+ */
543
+ stateCookiePrefix?: string;
544
+
499
545
  /**
500
546
  * Enable connection warmup to keep TCP+TLS alive after idle periods.
501
547
  *
@@ -507,6 +553,29 @@ export interface RangoOptions<TEnv = any> {
507
553
  */
508
554
  warmup?: boolean;
509
555
 
556
+ /**
557
+ * Wrap the hydrated client tree in `React.StrictMode`.
558
+ *
559
+ * The Rango browser entry hydrates the app inside `<React.StrictMode>` by
560
+ * default. StrictMode double-invokes render and (in development) mounts,
561
+ * unmounts, then remounts every effect to surface impure renders and missing
562
+ * effect cleanup. Production builds treat StrictMode as a no-op, so this flag
563
+ * only changes development behavior in a normal app.
564
+ *
565
+ * Set to `false` to hydrate without the StrictMode wrapper. The main reason to
566
+ * opt out is to isolate StrictMode's intentional double-render/double-effect
567
+ * from genuine re-renders when measuring client-hook stability — with
568
+ * StrictMode off, render counts are exact in development too.
569
+ *
570
+ * The value is resolved server-side at router creation and shipped to the
571
+ * client in the initial payload metadata; the browser entry reads it once at
572
+ * hydration. Changing it does not affect the SSR HTML (StrictMode emits no
573
+ * DOM), so toggling it never causes a hydration mismatch.
574
+ *
575
+ * @default true
576
+ */
577
+ strictMode?: boolean;
578
+
510
579
  /**
511
580
  * Shorthand timeout (ms) applied to both action execution and render start.
512
581
  * Does NOT apply to streamIdleMs.
@@ -559,11 +628,14 @@ export interface RangoOptions<TEnv = any> {
559
628
  onTimeout?: OnTimeoutCallback<TEnv>;
560
629
 
561
630
  /**
562
- * Telemetry sink for structured lifecycle events.
631
+ * Telemetry sink for structured, discrete lifecycle EVENTS: request
632
+ * start/end/error, loader start/end/error, handler errors, cache decisions,
633
+ * revalidation decisions, timeouts, origin rejections.
563
634
  *
564
- * When provided, the router emits events for request start/end,
565
- * loader start/end/error, handler errors, cache decisions, and
566
- * revalidation decisions.
635
+ * This is the EVENT surface. Phase-duration SPANS (request/middleware/action/
636
+ * handler/loader/render/ssr timing wired into a tracing backend) come from the
637
+ * separate `tracing` option below — a sink does not emit them, because async-context nesting
638
+ * cannot be faithfully reconstructed from after-the-fact start/end events.
567
639
  *
568
640
  * No-op when not configured (zero overhead).
569
641
  *
@@ -576,6 +648,18 @@ export interface RangoOptions<TEnv = any> {
576
648
  * });
577
649
  * ```
578
650
  *
651
+ * @example OpenTelemetry — pair the event sink with the tracing slot
652
+ * ```typescript
653
+ * import { createOTelTracing, createOTelSink } from "@rangojs/router";
654
+ * import { trace } from "@opentelemetry/api";
655
+ *
656
+ * const tracer = trace.getTracer("my-app");
657
+ * const router = createRouter({
658
+ * tracing: createOTelTracing(tracer), // phase spans
659
+ * telemetry: createOTelSink(tracer), // discrete-fact events
660
+ * });
661
+ * ```
662
+ *
579
663
  * @example Custom sink
580
664
  * ```typescript
581
665
  * const router = createRouter({
@@ -589,6 +673,44 @@ export interface RangoOptions<TEnv = any> {
589
673
  */
590
674
  telemetry?: TelemetrySink;
591
675
 
676
+ /**
677
+ * Span tracing for the router's performance phases (request, middleware, action,
678
+ * loaders, render, ssr). Connects the same phases shown in the
679
+ * `debugPerformance` timeline to the host platform's tracing system. This is
680
+ * the SPAN surface (the `telemetry` option above is the event surface).
681
+ *
682
+ * Two factories produce a config, both for this slot:
683
+ * - `createOTelTracing(tracer)` from `@rangojs/router` — any platform with an
684
+ * OpenTelemetry SDK (including Node). Bridges the phases onto
685
+ * `tracer.startActiveSpan`.
686
+ * - `createCloudflareTracing()` from `@rangojs/router/cloudflare` — Cloudflare
687
+ * Workers native custom spans, alongside the automatic KV/D1/fetch spans.
688
+ *
689
+ * When tracing is unset — or off-platform (no OTel SDK / no Cloudflare tracing
690
+ * destination) — every span call falls through to the work directly, so the
691
+ * request behaves exactly as if tracing were off.
692
+ *
693
+ * @example OpenTelemetry
694
+ * ```typescript
695
+ * import { createOTelTracing } from "@rangojs/router";
696
+ * import { trace } from "@opentelemetry/api";
697
+ *
698
+ * const router = createRouter({
699
+ * tracing: createOTelTracing(trace.getTracer("my-app")),
700
+ * });
701
+ * ```
702
+ *
703
+ * @example Cloudflare
704
+ * ```typescript
705
+ * import { createCloudflareTracing } from "@rangojs/router/cloudflare";
706
+ *
707
+ * const router = createRouter({
708
+ * tracing: createCloudflareTracing({ spans: { ssr: false } }),
709
+ * });
710
+ * ```
711
+ */
712
+ tracing?: RouterTracingConfig;
713
+
592
714
  /**
593
715
  * SSR configuration options.
594
716
  *
@@ -19,67 +19,29 @@ import type {
19
19
  } from "../../types";
20
20
  import type { SegmentResolutionDeps } from "../types.js";
21
21
  import { resolveLoaderData } from "./loader-cache.js";
22
- import { _getRequestContext } from "../../server/request-context.js";
23
- import { appendMetric } from "../metrics.js";
22
+ import {
23
+ isShellCaptureActive,
24
+ entryLoadingMasksLoaders,
25
+ } from "./loader-mask.js";
24
26
  import {
25
27
  handleHandlerResult,
26
28
  tryStaticHandler,
27
29
  tryStaticSlot,
28
30
  resolveLayoutComponent,
29
31
  resolveWithErrorBoundary,
32
+ warnOnStreamedResponse,
33
+ buildLoaderErrorContext,
30
34
  } from "./helpers.js";
31
35
  import { applyViewTransitionDefault } from "./view-transition-default.js";
32
36
  import { getRouterContext } from "../router-context.js";
33
- import { resolveSink, safeEmit } from "../telemetry.js";
37
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
38
+ import { observeHandler } from "../instrument.js";
34
39
  import {
35
40
  track,
36
41
  RangoContext,
37
42
  runInsideLoaderScope,
38
43
  } from "../../server/context.js";
39
44
 
40
- // ---------------------------------------------------------------------------
41
- // Streamed handler telemetry
42
- // ---------------------------------------------------------------------------
43
-
44
- /**
45
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
46
- * React catches the actual error via its error boundary; this only emits
47
- * the handler.error telemetry event.
48
- */
49
- function observeStreamedHandler(
50
- promise: Promise<ReactNode>,
51
- segmentId: string,
52
- segmentType: string,
53
- pathname?: string,
54
- routeKey?: string,
55
- params?: Record<string, string>,
56
- ): void {
57
- let routerCtx;
58
- try {
59
- routerCtx = getRouterContext();
60
- } catch {
61
- return;
62
- }
63
- if (!routerCtx?.telemetry) return;
64
- const sink = resolveSink(routerCtx.telemetry);
65
- const reqId = routerCtx.requestId;
66
- promise.catch((err: unknown) => {
67
- const errorObj = err instanceof Error ? err : new Error(String(err));
68
- safeEmit(sink, {
69
- type: "handler.error",
70
- timestamp: performance.now(),
71
- requestId: reqId,
72
- segmentId,
73
- segmentType,
74
- error: errorObj,
75
- handledByBoundary: true,
76
- pathname,
77
- routeKey,
78
- params,
79
- });
80
- });
81
- }
82
-
83
45
  // ---------------------------------------------------------------------------
84
46
  // Fresh path (full match, no revalidation)
85
47
  // ---------------------------------------------------------------------------
@@ -101,9 +63,33 @@ export async function resolveLoaders<TEnv>(
101
63
  const shortCode = shortCodeOverride ?? entry.shortCode;
102
64
  const hasLoading = "loading" in entry && entry.loading !== undefined;
103
65
  const loadingDisabled = hasLoading && entry.loading === false;
104
- const ms = _getRequestContext()?._metricsStore;
105
66
 
106
- if (!loadingDisabled) {
67
+ // Emit the streaming (non-awaiting) loader shape when loading is enabled OR
68
+ // during a PPR shell capture. In capture, LIVE-lane loaders are masked with
69
+ // never-resolving promises (loader-mask.ts); the loading-disabled branch below
70
+ // AWAITS the loader promises, which would hang the capture render's match()
71
+ // forever on those masked promises. Forcing the streaming shape lets match()
72
+ // complete so the prerender can postpone the loader subtrees as holes. The
73
+ // `!loadingDisabled` short-circuit keeps the ALS check off the hot path (only
74
+ // loading-disabled entries consult it), so normal requests are unchanged.
75
+ const emitStreaming = !loadingDisabled || isShellCaptureActive();
76
+
77
+ // PPR lane decision for this entry's loaders (loader-container-bake): an
78
+ // entry WITHOUT renderable loading() puts its loaders on the BAKE lane —
79
+ // executed at capture (container bakes, nested pending promises hole at the
80
+ // consumer's Suspense) and overlay-pinned from the shell snapshot on a HIT.
81
+ // Renderable loading() keeps the LIVE lane (masked at capture, always
82
+ // fresh). Computed per entry; resolveLoaderData applies the policy.
83
+ const bakeLane = !entryLoadingMasksLoaders(entry.loading);
84
+
85
+ // Error context for wrapLoaderPromise: without it, a throwing DSL loader never
86
+ // fires createRouter({ onError }) (phase "loader") nor emits the loader.error
87
+ // telemetry event — wrapLoaderPromise only builds the onError/telemetry path
88
+ // when errorContext is supplied. Built from ctx so the live render path reports
89
+ // loader failures the same way handlers/actions/routing/fetchable-loaders do.
90
+ const errorContext = buildLoaderErrorContext(ctx);
91
+
92
+ if (emitStreaming) {
107
93
  // Streaming loaders: promises kick off now, settle during RSC serialization.
108
94
  const segments = loaderEntries.map((loaderEntry, i) => {
109
95
  const { loader } = loaderEntry;
@@ -118,11 +104,17 @@ export async function resolveLoaders<TEnv>(
118
104
  loaderId: loader.$$id,
119
105
  loaderData: deps.wrapLoaderPromise(
120
106
  runInsideLoaderScope(() =>
121
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
107
+ resolveLoaderData(
108
+ loaderEntry,
109
+ ctx,
110
+ ctx.pathname,
111
+ bakeLane ? segmentId : null,
112
+ ),
122
113
  ),
123
114
  entry,
124
115
  segmentId,
125
116
  ctx.pathname,
117
+ errorContext,
126
118
  ),
127
119
  belongsToRoute,
128
120
  };
@@ -133,46 +125,51 @@ export async function resolveLoaders<TEnv>(
133
125
 
134
126
  // Loading disabled: still start all loaders in parallel, but only emit
135
127
  // settled promises so handlers don't stream loading placeholders.
136
- const pendingLoaderData = loaderEntries.map((loaderEntry) => {
137
- const start = performance.now();
138
- const promise = runInsideLoaderScope(() =>
139
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
128
+ //
129
+ // Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
130
+ // promise resolves to a LoaderDataResult and never rejects, routing a failed
131
+ // loader to its own per-loader error boundary. Awaiting the RAW promises here
132
+ // instead would (1) propagate a rejection to the segment-level boundary,
133
+ // collapsing the whole entry and discarding successful sibling data, and
134
+ // (2) leave the other in-flight raw promises without a .catch, producing
135
+ // unhandled rejections. Mirrors the loading path and intercept-resolution.
136
+ const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
137
+ const { loader } = loaderEntry;
138
+ const segmentId = `${shortCode}D${i}.${loader.$$id}`;
139
+ const wrapped = deps.wrapLoaderPromise(
140
+ runInsideLoaderScope(() =>
141
+ resolveLoaderData(
142
+ loaderEntry,
143
+ ctx,
144
+ ctx.pathname,
145
+ bakeLane ? segmentId : null,
146
+ ),
147
+ ),
148
+ entry,
149
+ segmentId,
150
+ ctx.pathname,
151
+ errorContext,
140
152
  );
141
- return { promise, start, loaderId: loaderEntry.loader.$$id };
153
+ return { wrapped, segmentId };
142
154
  });
143
- await Promise.all(pendingLoaderData.map((p) => p.promise));
155
+ await Promise.all(pendingLoaderData.map((p) => p.wrapped));
144
156
 
145
157
  return loaderEntries.map((loaderEntry, i) => {
146
158
  const { loader } = loaderEntry;
147
- const segmentId = `${shortCode}D${i}.${loader.$$id}`;
148
159
  const pending = pendingLoaderData[i]!;
149
- if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
150
- // All loaders ran in parallel via Promise.all — each span covers
151
- // from its own kickoff to the batch settlement, giving a ceiling
152
- // on that loader's contribution to the overall wait.
153
- const batchEnd = performance.now();
154
- appendMetric(
155
- ms,
156
- `loader:${loader.$$id}`,
157
- pending.start,
158
- batchEnd - pending.start,
159
- 2,
160
- );
161
- }
160
+ // The "loader:<id>" perf metric is recorded by observePhase at the single
161
+ // loader-metering site (useLoader, reached via ctx.use during
162
+ // resolveLoaderData), with the real per-loader duration rather than a
163
+ // Promise.all batch ceiling.
162
164
  return {
163
- id: segmentId,
165
+ id: pending.segmentId,
164
166
  namespace: entry.id,
165
167
  type: "loader" as const,
166
168
  index: i,
167
169
  component: null,
168
170
  params: ctx.params,
169
171
  loaderId: loader.$$id,
170
- loaderData: deps.wrapLoaderPromise(
171
- pending.promise,
172
- entry,
173
- segmentId,
174
- ctx.pathname,
175
- ),
172
+ loaderData: pending.wrapped,
176
173
  belongsToRoute,
177
174
  };
178
175
  });
@@ -184,6 +181,15 @@ export async function resolveLoaders<TEnv>(
184
181
  export interface ResolveSegmentOptions {
185
182
  /** When true, skip resolveLoaders() calls (used for pre-rendering) */
186
183
  skipLoaders?: boolean;
184
+ /**
185
+ * When true, a thrown render error is re-thrown instead of being converted
186
+ * into an error-boundary segment. Set only by the pre-render path so a
187
+ * build-time render failure (and a `throw new Skip()` inside a render fn)
188
+ * surfaces to the build instead of being silently baked into a frozen error
189
+ * page served as a 200 (issue #587). The live request path leaves this unset,
190
+ * so error boundaries keep catching at request time.
191
+ */
192
+ throwOnError?: boolean;
187
193
  }
188
194
 
189
195
  /**
@@ -228,6 +234,7 @@ export async function resolveSegment<TEnv>(
228
234
  transition: applyViewTransitionDefault(
229
235
  entry.transition,
230
236
  deps.viewTransitionDefault,
237
+ entry.shortCode,
231
238
  ),
232
239
  params,
233
240
  belongsToRoute: false,
@@ -295,8 +302,11 @@ export async function resolveSegment<TEnv>(
295
302
  !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
296
303
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
297
304
  if (entry.loading) {
298
- const result = handleHandlerResult(handler(context));
305
+ const result = handleHandlerResult(
306
+ observeHandler(entry.id, handler, context),
307
+ );
299
308
  if (result instanceof Promise) {
309
+ warnOnStreamedResponse(result, entry.id);
300
310
  result.finally(doneRouteHandler).catch(() => {});
301
311
  const tracked = deps.trackHandler(result, {
302
312
  segmentId: entry.shortCode,
@@ -316,7 +326,9 @@ export async function resolveSegment<TEnv>(
316
326
  component = result;
317
327
  }
318
328
  } else {
319
- component = handleHandlerResult(await handler(context));
329
+ component = handleHandlerResult(
330
+ await observeHandler(entry.id, handler, context),
331
+ );
320
332
  doneRouteHandler();
321
333
  }
322
334
  }
@@ -366,6 +378,7 @@ export async function resolveSegment<TEnv>(
366
378
  transition: applyViewTransitionDefault(
367
379
  entry.transition,
368
380
  deps.viewTransitionDefault,
381
+ entry.shortCode,
369
382
  ),
370
383
  params,
371
384
  belongsToRoute: true,
@@ -453,6 +466,7 @@ export async function resolveOrphanLayout<TEnv>(
453
466
  transition: applyViewTransitionDefault(
454
467
  orphan.transition,
455
468
  deps.viewTransitionDefault,
469
+ orphan.shortCode,
456
470
  ),
457
471
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
458
472
  });
@@ -541,7 +555,9 @@ export async function resolveParallelEntry<TEnv>(
541
555
  parallelEntry.loading !== undefined && parallelEntry.loading !== false;
542
556
  if (hasLoadingFallback) {
543
557
  const result =
544
- typeof handler === "function" ? handler(context) : handler;
558
+ typeof handler === "function"
559
+ ? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
560
+ : handler;
545
561
  if (result instanceof Promise) {
546
562
  result.finally(doneParallelHandler).catch(() => {});
547
563
  const tracked = deps.trackHandler(result, {
@@ -563,7 +579,13 @@ export async function resolveParallelEntry<TEnv>(
563
579
  }
564
580
  } else {
565
581
  component =
566
- typeof handler === "function" ? await handler(context) : handler;
582
+ typeof handler === "function"
583
+ ? await observeHandler(
584
+ `${parallelEntry.id}.${slot}`,
585
+ handler,
586
+ context,
587
+ )
588
+ : handler;
567
589
  doneParallelHandler();
568
590
  }
569
591
  }
@@ -578,6 +600,7 @@ export async function resolveParallelEntry<TEnv>(
578
600
  transition: applyViewTransitionDefault(
579
601
  parallelEntry.transition,
580
602
  deps.viewTransitionDefault,
603
+ `${parentShortCode}.${slot}`,
581
604
  ),
582
605
  params,
583
606
  slot,
@@ -667,6 +690,7 @@ export async function resolveAllSegments<TEnv>(
667
690
  deps,
668
691
  { request: safeRequest, url: context.url, routeKey, telemetry },
669
692
  context.pathname,
693
+ options?.throwOnError,
670
694
  );
671
695
  doneEntry();
672
696
  // Deduplicate by segment ID. include() scopes can produce entries that
@@ -19,13 +19,48 @@ import {
19
19
  import { getRequestContext } from "../../server/request-context.js";
20
20
  import { DefaultErrorFallback } from "../../default-error-boundary.js";
21
21
  import type { EntryData } from "../../server/context";
22
- import type { ResolvedSegment, ErrorInfo, HandlerContext } from "../../types";
22
+ import type {
23
+ ResolvedSegment,
24
+ ErrorInfo,
25
+ HandlerContext,
26
+ InternalHandlerContext,
27
+ } from "../../types";
23
28
  import type { SegmentResolutionDeps } from "../types.js";
24
29
  import { debugLog } from "../logging.js";
25
30
  import { tryStaticLookup } from "./static-store.js";
31
+ import { observeHandler } from "../instrument.js";
26
32
  import type { TelemetrySink } from "../telemetry.js";
27
33
  import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
28
34
 
35
+ /** The errorContext shape wrapLoaderPromise expects as its 5th argument. */
36
+ type LoaderErrorContext<TEnv> = NonNullable<
37
+ Parameters<SegmentResolutionDeps<TEnv>["wrapLoaderPromise"]>[4]
38
+ >;
39
+
40
+ /**
41
+ * Build the errorContext passed to wrapLoaderPromise so a throwing DSL loader
42
+ * fires createRouter({ onError }) (phase "loader") and emits the loader.error
43
+ * telemetry event. wrapLoaderPromise only wires the onError/telemetry path when
44
+ * this 5th argument is present; every real call site previously omitted it, so
45
+ * loaders were the one phase whose failures were silently dropped (handlers,
46
+ * actions, routing, rendering, and fetchable loaders all reported correctly).
47
+ *
48
+ * The fields come off the handler context, which already carries the request,
49
+ * url, params, env, and (on the internal shape) the matched route name.
50
+ */
51
+ export function buildLoaderErrorContext<TEnv>(
52
+ ctx: HandlerContext<any, TEnv>,
53
+ ): LoaderErrorContext<TEnv> {
54
+ const internal = ctx as InternalHandlerContext<any, TEnv>;
55
+ return {
56
+ request: ctx.request,
57
+ url: ctx.url,
58
+ routeKey: internal._routeName,
59
+ params: ctx.params as Record<string, string>,
60
+ env: ctx.env,
61
+ };
62
+ }
63
+
29
64
  // ---------------------------------------------------------------------------
30
65
  // Handler result processing
31
66
  // ---------------------------------------------------------------------------
@@ -52,6 +87,40 @@ export function handleHandlerResult(
52
87
  return result;
53
88
  }
54
89
 
90
+ /**
91
+ * Dev-only: warn when a handler on a route that declares loading() resolves or
92
+ * rejects with a Response (e.g. redirect()).
93
+ *
94
+ * On a non-loading route a returned/thrown Response short-circuits to an HTTP
95
+ * redirect. But when the route declares loading(), the handler result is
96
+ * streamed (not awaited at the resolution boundary), so the Response surfaces
97
+ * only during RSC serialization and is rendered into the stream instead of
98
+ * becoming a 302/308 — a silent failure mode. Issue redirects from middleware,
99
+ * a loader, or a synchronous handler return instead. Compiled out in production.
100
+ */
101
+ export function warnOnStreamedResponse(
102
+ result: Promise<unknown>,
103
+ entryId: string,
104
+ ): void {
105
+ if (process.env.NODE_ENV === "production") return;
106
+ // A Response can surface either as a rejection (handleHandlerResult rethrows a
107
+ // resolved Response) or as a resolved value (the raw parallel-slot handler is
108
+ // not run through handleHandlerResult). Check both so every streamed path is
109
+ // covered. Each handler is an independent observer; it does not consume the
110
+ // rejection for the trackHandler/observeStreamedHandler chains.
111
+ const check = (value: unknown) => {
112
+ if (value instanceof Response) {
113
+ console.warn(
114
+ `[rango] Handler for "${entryId}" returned a Response (e.g. ` +
115
+ `redirect()), but it declares loading(): the Response is rendered ` +
116
+ `into the RSC stream, NOT sent as an HTTP redirect. Issue redirects ` +
117
+ `from middleware, a loader, or a synchronous handler return.`,
118
+ );
119
+ }
120
+ };
121
+ result.then(check, check);
122
+ }
123
+
55
124
  // ---------------------------------------------------------------------------
56
125
  // Static handler interception
57
126
  // ---------------------------------------------------------------------------
@@ -96,11 +165,16 @@ export async function resolveLayoutComponent<TEnv>(
96
165
  entry: EntryData,
97
166
  context: HandlerContext<any, TEnv>,
98
167
  ): Promise<ReactNode> {
99
- const component = await tryStaticHandler(entry, entry.shortCode);
100
- if (component !== undefined) return component;
101
- return typeof entry.handler === "function"
102
- ? handleHandlerResult(await entry.handler(context))
103
- : (entry.handler as ReactNode);
168
+ // Static/prerender hit: no handler runs, so emit no rango.handler span.
169
+ const staticComponent = await tryStaticHandler(entry, entry.shortCode);
170
+ if (staticComponent !== undefined) return staticComponent;
171
+ const handler = entry.handler;
172
+ if (typeof handler !== "function") return handler as ReactNode;
173
+ // Wrap ONLY the handler call in the rango.handler span (the perf metric is owned
174
+ // by track("handler:<id>") at the call site). handleHandlerResult stays OUTSIDE
175
+ // the span so a handler that returns a Response (redirect control flow, which it
176
+ // rethrows) is not recorded as a span error — mirrors the route-handler sites.
177
+ return handleHandlerResult(await observeHandler(entry.id, handler, context));
104
178
  }
105
179
 
106
180
  // ---------------------------------------------------------------------------
@@ -250,11 +324,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
250
324
  deps: SegmentResolutionDeps<TEnv>,
251
325
  report?: ErrorReportContext,
252
326
  pathname?: string,
327
+ throwOnError?: boolean,
253
328
  ): Promise<TResult> {
254
329
  try {
255
330
  return await resolveFn();
256
331
  } catch (error) {
257
332
  if (error instanceof Response) throw error;
333
+ // Pre-render surfaces render failures to the build instead of baking the
334
+ // error boundary as a frozen 200 (issue #587). A `throw new Skip()` in a
335
+ // render fn also propagates here so the build can skip that URL rather than
336
+ // bake its error page. The live request path leaves throwOnError unset.
337
+ if (throwOnError) throw error;
258
338
  const segment = catchSegmentError(
259
339
  error,
260
340
  entry,