@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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 (404) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3386 -1111
  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 +78 -25
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +247 -23
  13. package/skills/caching/SKILL.md +281 -11
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +289 -53
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +914 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +144 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +101 -5
  37. package/skills/router-setup/SKILL.md +117 -10
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +332 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +199 -86
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +174 -109
  80. package/src/browser/prefetch/cache.ts +222 -69
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +113 -32
  83. package/src/browser/prefetch/resource-ready.ts +77 -0
  84. package/src/browser/rango-state.ts +158 -76
  85. package/src/browser/react/Link.tsx +102 -15
  86. package/src/browser/react/NavigationProvider.tsx +300 -122
  87. package/src/browser/react/ScrollRestoration.tsx +10 -6
  88. package/src/browser/react/context.ts +7 -2
  89. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  90. package/src/browser/react/filter-segment-order.ts +66 -7
  91. package/src/browser/react/index.ts +0 -48
  92. package/src/browser/react/location-state-shared.ts +178 -8
  93. package/src/browser/react/location-state.ts +39 -14
  94. package/src/browser/react/use-action.ts +6 -15
  95. package/src/browser/react/use-handle.ts +23 -69
  96. package/src/browser/react/use-href.tsx +8 -1
  97. package/src/browser/react/use-link-status.ts +33 -8
  98. package/src/browser/react/use-navigation.ts +32 -7
  99. package/src/browser/react/use-params.ts +20 -10
  100. package/src/browser/react/use-reverse.ts +106 -0
  101. package/src/browser/react/use-router.ts +46 -11
  102. package/src/browser/react/use-search-params.ts +0 -5
  103. package/src/browser/react/use-segments.ts +11 -21
  104. package/src/browser/response-adapter.ts +99 -8
  105. package/src/browser/rsc-router.tsx +125 -28
  106. package/src/browser/scroll-restoration.ts +37 -22
  107. package/src/browser/segment-reconciler.ts +36 -14
  108. package/src/browser/segment-structure-assert.ts +2 -2
  109. package/src/browser/server-action-bridge.ts +222 -61
  110. package/src/browser/types.ts +115 -12
  111. package/src/browser/validate-redirect-origin.ts +43 -16
  112. package/src/build/collect-fallback-refs.ts +107 -0
  113. package/src/build/generate-manifest.ts +65 -40
  114. package/src/build/generate-route-types.ts +5 -1
  115. package/src/build/index.ts +8 -2
  116. package/src/build/prefix-tree-utils.ts +123 -0
  117. package/src/build/route-trie.ts +165 -36
  118. package/src/build/route-types/ast-route-extraction.ts +15 -8
  119. package/src/build/route-types/codegen.ts +16 -5
  120. package/src/build/route-types/include-resolution.ts +125 -24
  121. package/src/build/route-types/param-extraction.ts +6 -3
  122. package/src/build/route-types/per-module-writer.ts +22 -6
  123. package/src/build/route-types/router-processing.ts +260 -94
  124. package/src/build/route-types/scan-filter.ts +9 -2
  125. package/src/build/route-types/source-scan.ts +216 -0
  126. package/src/build/runtime-discovery.ts +9 -20
  127. package/src/cache/cache-error.ts +104 -0
  128. package/src/cache/cache-key-utils.ts +29 -13
  129. package/src/cache/cache-policy.ts +108 -34
  130. package/src/cache/cache-runtime.ts +239 -52
  131. package/src/cache/cache-scope.ts +234 -87
  132. package/src/cache/cache-tag.ts +103 -0
  133. package/src/cache/cf/cf-base64.ts +33 -0
  134. package/src/cache/cf/cf-cache-constants.ts +127 -0
  135. package/src/cache/cf/cf-cache-store.ts +1989 -378
  136. package/src/cache/cf/cf-cache-types.ts +349 -0
  137. package/src/cache/cf/cf-kv-utils.ts +46 -0
  138. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  139. package/src/cache/cf/index.ts +6 -16
  140. package/src/cache/document-cache.ts +89 -21
  141. package/src/cache/handle-snapshot.ts +70 -0
  142. package/src/cache/index.ts +10 -20
  143. package/src/cache/memory-segment-store.ts +136 -37
  144. package/src/cache/profile-registry.ts +46 -31
  145. package/src/cache/read-through-swr.ts +56 -12
  146. package/src/cache/segment-codec.ts +9 -17
  147. package/src/cache/tag-invalidation.ts +230 -0
  148. package/src/cache/taint.ts +55 -0
  149. package/src/cache/types.ts +37 -100
  150. package/src/client.rsc.tsx +44 -21
  151. package/src/client.tsx +119 -290
  152. package/src/cloudflare/index.ts +11 -0
  153. package/src/cloudflare/tracing.ts +109 -0
  154. package/src/component-utils.ts +19 -0
  155. package/src/components/DefaultDocument.tsx +8 -2
  156. package/src/context-var.ts +84 -2
  157. package/src/decode-loader-results.ts +52 -0
  158. package/src/defer.ts +196 -0
  159. package/src/deps/ssr.ts +0 -1
  160. package/src/encode-kv.ts +49 -0
  161. package/src/errors.ts +30 -4
  162. package/src/escape-script.ts +52 -0
  163. package/src/handle.ts +70 -22
  164. package/src/handles/MetaTags.tsx +62 -19
  165. package/src/handles/Scripts.tsx +183 -0
  166. package/src/handles/breadcrumbs.ts +37 -8
  167. package/src/handles/is-thenable.ts +19 -0
  168. package/src/handles/meta.ts +51 -40
  169. package/src/handles/script.ts +244 -0
  170. package/src/host/cookie-handler.ts +9 -60
  171. package/src/host/errors.ts +0 -24
  172. package/src/host/index.ts +8 -2
  173. package/src/host/pattern-matcher.ts +23 -52
  174. package/src/host/router.ts +107 -99
  175. package/src/host/testing.ts +40 -27
  176. package/src/host/types.ts +37 -4
  177. package/src/host/utils.ts +1 -1
  178. package/src/href-client.ts +137 -22
  179. package/src/index.rsc.ts +99 -13
  180. package/src/index.ts +139 -19
  181. package/src/internal-debug.ts +11 -10
  182. package/src/loader-store.ts +500 -0
  183. package/src/loader.rsc.ts +20 -13
  184. package/src/loader.ts +12 -11
  185. package/src/missing-id-error.ts +68 -0
  186. package/src/outlet-context.ts +1 -1
  187. package/src/outlet-provider.tsx +1 -5
  188. package/src/prerender/param-hash.ts +16 -16
  189. package/src/prerender/store.ts +37 -41
  190. package/src/prerender.ts +198 -82
  191. package/src/redirect-origin.ts +100 -0
  192. package/src/regex-escape.ts +8 -0
  193. package/src/render-error-thrower.tsx +20 -0
  194. package/src/response-utils.ts +62 -0
  195. package/src/reverse.ts +65 -15
  196. package/src/root-error-boundary.tsx +1 -19
  197. package/src/route-content-wrapper.tsx +19 -77
  198. package/src/route-definition/dsl-helpers.ts +461 -304
  199. package/src/route-definition/helper-factories.ts +28 -140
  200. package/src/route-definition/helpers-types.ts +149 -74
  201. package/src/route-definition/index.ts +4 -2
  202. package/src/route-definition/redirect.ts +51 -10
  203. package/src/route-definition/resolve-handler-use.ts +160 -0
  204. package/src/route-definition/use-item-types.ts +29 -0
  205. package/src/route-map-builder.ts +0 -16
  206. package/src/route-types.ts +37 -46
  207. package/src/router/basename.ts +14 -0
  208. package/src/router/content-negotiation.ts +164 -17
  209. package/src/router/error-handling.ts +45 -18
  210. package/src/router/find-match.ts +44 -23
  211. package/src/router/handler-context.ts +83 -39
  212. package/src/router/instrument.ts +350 -0
  213. package/src/router/intercept-resolution.ts +48 -24
  214. package/src/router/lazy-includes.ts +15 -52
  215. package/src/router/loader-resolution.ts +274 -56
  216. package/src/router/logging.ts +0 -6
  217. package/src/router/manifest.ts +40 -42
  218. package/src/router/match-api.ts +124 -204
  219. package/src/router/match-context.ts +0 -22
  220. package/src/router/match-handlers.ts +58 -58
  221. package/src/router/match-middleware/background-revalidation.ts +54 -25
  222. package/src/router/match-middleware/cache-lookup.ts +205 -271
  223. package/src/router/match-middleware/cache-store.ts +81 -50
  224. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  225. package/src/router/match-middleware/segment-resolution.ts +45 -14
  226. package/src/router/match-pipelines.ts +1 -42
  227. package/src/router/match-result.ts +93 -39
  228. package/src/router/metrics.ts +5 -34
  229. package/src/router/middleware-types.ts +13 -142
  230. package/src/router/middleware.ts +268 -171
  231. package/src/router/navigation-snapshot.ts +131 -0
  232. package/src/router/params-util.ts +23 -0
  233. package/src/router/pattern-matching.ts +132 -90
  234. package/src/router/prefetch-cache-ttl.ts +51 -0
  235. package/src/router/prefetch-limits.ts +37 -0
  236. package/src/router/prerender-match.ts +195 -56
  237. package/src/router/preview-match.ts +32 -102
  238. package/src/router/request-classification.ts +276 -0
  239. package/src/router/revalidation.ts +123 -73
  240. package/src/router/route-snapshot.ts +244 -0
  241. package/src/router/router-context.ts +3 -27
  242. package/src/router/router-interfaces.ts +129 -35
  243. package/src/router/router-options.ts +202 -15
  244. package/src/router/router-registry.ts +2 -5
  245. package/src/router/segment-resolution/fresh.ts +197 -92
  246. package/src/router/segment-resolution/helpers.ts +115 -30
  247. package/src/router/segment-resolution/loader-cache.ts +76 -39
  248. package/src/router/segment-resolution/revalidation.ts +392 -338
  249. package/src/router/segment-resolution/static-store.ts +19 -5
  250. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  251. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  252. package/src/router/segment-resolution.ts +5 -1
  253. package/src/router/segment-wrappers.ts +6 -5
  254. package/src/router/state-cookie-name.ts +33 -0
  255. package/src/router/substitute-pattern-params.ts +56 -0
  256. package/src/router/telemetry-otel.ts +161 -199
  257. package/src/router/telemetry.ts +96 -19
  258. package/src/router/timeout.ts +0 -20
  259. package/src/router/tracing.ts +206 -0
  260. package/src/router/trie-matching.ts +163 -59
  261. package/src/router/types.ts +10 -63
  262. package/src/router/url-params.ts +44 -0
  263. package/src/router.ts +174 -54
  264. package/src/rsc/handler-context.ts +3 -2
  265. package/src/rsc/handler.ts +660 -518
  266. package/src/rsc/helpers.ts +168 -46
  267. package/src/rsc/index.ts +2 -5
  268. package/src/rsc/json-route-result.ts +38 -0
  269. package/src/rsc/loader-fetch.ts +127 -31
  270. package/src/rsc/manifest-init.ts +33 -42
  271. package/src/rsc/origin-guard.ts +39 -25
  272. package/src/rsc/progressive-enhancement.ts +133 -13
  273. package/src/rsc/redirect-guard.ts +99 -0
  274. package/src/rsc/response-cache-serve.ts +238 -0
  275. package/src/rsc/response-error.ts +79 -12
  276. package/src/rsc/response-route-handler.ts +99 -189
  277. package/src/rsc/rsc-rendering.ts +115 -73
  278. package/src/rsc/runtime-warnings.ts +23 -10
  279. package/src/rsc/server-action.ts +287 -113
  280. package/src/rsc/ssr-setup.ts +18 -2
  281. package/src/rsc/transition-gate.ts +89 -0
  282. package/src/rsc/types.ts +36 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +35 -30
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +269 -202
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +235 -51
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +38 -46
  294. package/src/server/request-context.ts +442 -173
  295. package/src/ssr/index.tsx +24 -16
  296. package/src/static-handler.ts +27 -18
  297. package/src/testing/cache-status.ts +162 -0
  298. package/src/testing/collect-handle.ts +40 -0
  299. package/src/testing/dispatch.ts +701 -0
  300. package/src/testing/dom.entry.ts +22 -0
  301. package/src/testing/e2e/fixture.ts +188 -0
  302. package/src/testing/e2e/index.ts +128 -0
  303. package/src/testing/e2e/matchers.ts +35 -0
  304. package/src/testing/e2e/page-helpers.ts +272 -0
  305. package/src/testing/e2e/parity.ts +387 -0
  306. package/src/testing/e2e/server.ts +195 -0
  307. package/src/testing/flight-matchers.ts +97 -0
  308. package/src/testing/flight-normalize.ts +11 -0
  309. package/src/testing/flight-runtime.d.ts +57 -0
  310. package/src/testing/flight-tree.ts +682 -0
  311. package/src/testing/flight.entry.ts +52 -0
  312. package/src/testing/flight.ts +257 -0
  313. package/src/testing/generated-routes.ts +183 -0
  314. package/src/testing/index.ts +105 -0
  315. package/src/testing/internal/context.ts +371 -0
  316. package/src/testing/internal/flight-client-globals.ts +30 -0
  317. package/src/testing/internal/seed-vars.ts +54 -0
  318. package/src/testing/render-handler.ts +357 -0
  319. package/src/testing/render-route.tsx +581 -0
  320. package/src/testing/run-loader.ts +385 -0
  321. package/src/testing/run-middleware.ts +205 -0
  322. package/src/testing/run-transition-when.ts +164 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +0 -7
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +234 -82
  340. package/src/types/index.ts +3 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +6 -50
  344. package/src/types/route-entry.ts +12 -7
  345. package/src/types/segments.ts +136 -15
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +68 -18
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -19
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  357. package/src/vite/discovery/discover-routers.ts +130 -85
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +214 -132
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +57 -4
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +6 -0
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +178 -5
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +48 -95
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +89 -27
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +40 -29
  385. package/src/vite/plugins/version-plugin.ts +37 -40
  386. package/src/vite/plugins/virtual-entries.ts +39 -25
  387. package/src/vite/rango.ts +119 -111
  388. package/src/vite/router-discovery.ts +941 -142
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +1 -1
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +81 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/src/browser/action-response-classifier.ts +0 -99
  400. package/src/browser/react/use-client-cache.ts +0 -58
  401. package/src/browser/shallow.ts +0 -40
  402. package/src/handles/index.ts +0 -7
  403. package/src/network-error-thrower.tsx +0 -23
  404. package/src/router/middleware-cookies.ts +0 -55
@@ -19,61 +19,24 @@ 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";
24
22
  import {
25
23
  handleHandlerResult,
26
24
  tryStaticHandler,
27
25
  tryStaticSlot,
28
26
  resolveLayoutComponent,
29
27
  resolveWithErrorBoundary,
28
+ warnOnStreamedResponse,
29
+ buildLoaderErrorContext,
30
30
  } from "./helpers.js";
31
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
31
32
  import { getRouterContext } from "../router-context.js";
32
- import { resolveSink, safeEmit } from "../telemetry.js";
33
- import { track } from "../../server/context.js";
34
-
35
- // ---------------------------------------------------------------------------
36
- // Streamed handler telemetry
37
- // ---------------------------------------------------------------------------
38
-
39
- /**
40
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
41
- * React catches the actual error via its error boundary; this only emits
42
- * the handler.error telemetry event.
43
- */
44
- function observeStreamedHandler(
45
- promise: Promise<ReactNode>,
46
- segmentId: string,
47
- segmentType: string,
48
- pathname?: string,
49
- routeKey?: string,
50
- params?: Record<string, string>,
51
- ): void {
52
- let routerCtx;
53
- try {
54
- routerCtx = getRouterContext();
55
- } catch {
56
- return;
57
- }
58
- if (!routerCtx?.telemetry) return;
59
- const sink = resolveSink(routerCtx.telemetry);
60
- const reqId = routerCtx.requestId;
61
- promise.catch((err: unknown) => {
62
- const errorObj = err instanceof Error ? err : new Error(String(err));
63
- safeEmit(sink, {
64
- type: "handler.error",
65
- timestamp: performance.now(),
66
- requestId: reqId,
67
- segmentId,
68
- segmentType,
69
- error: errorObj,
70
- handledByBoundary: true,
71
- pathname,
72
- routeKey,
73
- params,
74
- });
75
- });
76
- }
33
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
34
+ import { observeHandler } from "../instrument.js";
35
+ import {
36
+ track,
37
+ RangoContext,
38
+ runInsideLoaderScope,
39
+ } from "../../server/context.js";
77
40
 
78
41
  // ---------------------------------------------------------------------------
79
42
  // Fresh path (full match, no revalidation)
@@ -96,12 +59,16 @@ export async function resolveLoaders<TEnv>(
96
59
  const shortCode = shortCodeOverride ?? entry.shortCode;
97
60
  const hasLoading = "loading" in entry && entry.loading !== undefined;
98
61
  const loadingDisabled = hasLoading && entry.loading === false;
99
- const ms = _getRequestContext()?._metricsStore;
62
+
63
+ // Error context for wrapLoaderPromise: without it, a throwing DSL loader never
64
+ // fires createRouter({ onError }) (phase "loader") nor emits the loader.error
65
+ // telemetry event — wrapLoaderPromise only builds the onError/telemetry path
66
+ // when errorContext is supplied. Built from ctx so the live render path reports
67
+ // loader failures the same way handlers/actions/routing/fetchable-loaders do.
68
+ const errorContext = buildLoaderErrorContext(ctx);
100
69
 
101
70
  if (!loadingDisabled) {
102
71
  // Streaming loaders: promises kick off now, settle during RSC serialization.
103
- // Record each loader as a marker span showing it was invoked.
104
- const loaderStart = performance.now();
105
72
  const segments = loaderEntries.map((loaderEntry, i) => {
106
73
  const { loader } = loaderEntry;
107
74
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
@@ -114,53 +81,63 @@ export async function resolveLoaders<TEnv>(
114
81
  params: ctx.params,
115
82
  loaderId: loader.$$id,
116
83
  loaderData: deps.wrapLoaderPromise(
117
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
84
+ runInsideLoaderScope(() =>
85
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
86
+ ),
118
87
  entry,
119
88
  segmentId,
120
89
  ctx.pathname,
90
+ errorContext,
121
91
  ),
122
92
  belongsToRoute,
123
93
  };
124
94
  });
125
- if (ms) {
126
- const dur = performance.now() - loaderStart;
127
- for (const seg of segments) {
128
- appendMetric(ms, `loader:${seg.loaderId}`, loaderStart, dur, 2);
129
- }
130
- }
95
+
131
96
  return segments;
132
97
  }
133
98
 
134
99
  // Loading disabled: still start all loaders in parallel, but only emit
135
100
  // settled promises so handlers don't stream loading placeholders.
136
- // We can measure actual execution time here since we await all loaders.
137
- const loaderStart = performance.now();
138
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
139
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
140
- );
141
- await Promise.all(pendingLoaderData);
142
- const loaderDur = performance.now() - loaderStart;
101
+ //
102
+ // Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
103
+ // promise resolves to a LoaderDataResult and never rejects, routing a failed
104
+ // loader to its own per-loader error boundary. Awaiting the RAW promises here
105
+ // instead would (1) propagate a rejection to the segment-level boundary,
106
+ // collapsing the whole entry and discarding successful sibling data, and
107
+ // (2) leave the other in-flight raw promises without a .catch, producing
108
+ // unhandled rejections. Mirrors the loading path and intercept-resolution.
109
+ const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
110
+ const { loader } = loaderEntry;
111
+ const segmentId = `${shortCode}D${i}.${loader.$$id}`;
112
+ const wrapped = deps.wrapLoaderPromise(
113
+ runInsideLoaderScope(() =>
114
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
115
+ ),
116
+ entry,
117
+ segmentId,
118
+ ctx.pathname,
119
+ errorContext,
120
+ );
121
+ return { wrapped, segmentId };
122
+ });
123
+ await Promise.all(pendingLoaderData.map((p) => p.wrapped));
143
124
 
144
125
  return loaderEntries.map((loaderEntry, i) => {
145
126
  const { loader } = loaderEntry;
146
- const segmentId = `${shortCode}D${i}.${loader.$$id}`;
147
- if (ms) {
148
- appendMetric(ms, `loader:${loader.$$id}`, loaderStart, loaderDur, 2);
149
- }
127
+ const pending = pendingLoaderData[i]!;
128
+ // The "loader:<id>" perf metric is recorded by observePhase at the single
129
+ // loader-metering site (useLoader, reached via ctx.use during
130
+ // resolveLoaderData), with the real per-loader duration rather than a
131
+ // Promise.all batch ceiling.
150
132
  return {
151
- id: segmentId,
133
+ id: pending.segmentId,
152
134
  namespace: entry.id,
153
135
  type: "loader" as const,
154
136
  index: i,
155
137
  component: null,
156
138
  params: ctx.params,
157
139
  loaderId: loader.$$id,
158
- loaderData: deps.wrapLoaderPromise(
159
- pendingLoaderData[i]!,
160
- entry,
161
- segmentId,
162
- ctx.pathname,
163
- ),
140
+ loaderData: pending.wrapped,
164
141
  belongsToRoute,
165
142
  };
166
143
  });
@@ -172,6 +149,15 @@ export async function resolveLoaders<TEnv>(
172
149
  export interface ResolveSegmentOptions {
173
150
  /** When true, skip resolveLoaders() calls (used for pre-rendering) */
174
151
  skipLoaders?: boolean;
152
+ /**
153
+ * When true, a thrown render error is re-thrown instead of being converted
154
+ * into an error-boundary segment. Set only by the pre-render path so a
155
+ * build-time render failure (and a `throw new Skip()` inside a render fn)
156
+ * surfaces to the build instead of being silently baked into a frozen error
157
+ * page served as a 200 (issue #587). The live request path leaves this unset,
158
+ * so error boundaries keep catching at request time.
159
+ */
160
+ throwOnError?: boolean;
175
161
  }
176
162
 
177
163
  /**
@@ -213,7 +199,11 @@ export async function resolveSegment<TEnv>(
213
199
  index: 0,
214
200
  component,
215
201
  loading: entry.loading === false ? null : entry.loading,
216
- transition: entry.transition,
202
+ transition: applyViewTransitionDefault(
203
+ entry.transition,
204
+ deps.viewTransitionDefault,
205
+ entry.shortCode,
206
+ ),
217
207
  params,
218
208
  belongsToRoute: false,
219
209
  layoutName: entry.id,
@@ -273,10 +263,18 @@ export async function resolveSegment<TEnv>(
273
263
  entry.shortCode,
274
264
  );
275
265
  if (component === undefined) {
266
+ // For Passthrough routes at runtime, use the live handler instead of
267
+ // the build handler. At build time (context.build === true), always
268
+ // use the build handler from entry.handler.
269
+ const handler =
270
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
276
271
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
277
272
  if (entry.loading) {
278
- const result = handleHandlerResult(entry.handler(context));
273
+ const result = handleHandlerResult(
274
+ observeHandler(entry.id, handler, context),
275
+ );
279
276
  if (result instanceof Promise) {
277
+ warnOnStreamedResponse(result, entry.id);
280
278
  result.finally(doneRouteHandler).catch(() => {});
281
279
  const tracked = deps.trackHandler(result, {
282
280
  segmentId: entry.shortCode,
@@ -296,7 +294,9 @@ export async function resolveSegment<TEnv>(
296
294
  component = result;
297
295
  }
298
296
  } else {
299
- component = handleHandlerResult(await entry.handler(context));
297
+ component = handleHandlerResult(
298
+ await observeHandler(entry.id, handler, context),
299
+ );
300
300
  doneRouteHandler();
301
301
  }
302
302
  }
@@ -311,6 +311,7 @@ export async function resolveSegment<TEnv>(
311
311
  deps,
312
312
  options,
313
313
  routeKey,
314
+ entry,
314
315
  );
315
316
  segments.push(...orphanSegments);
316
317
  }
@@ -340,9 +341,13 @@ export async function resolveSegment<TEnv>(
340
341
  namespace: entry.id,
341
342
  type: "route",
342
343
  index: 0,
343
- component,
344
+ component: component ?? null,
344
345
  loading: entry.loading === false ? null : entry.loading,
345
- transition: entry.transition,
346
+ transition: applyViewTransitionDefault(
347
+ entry.transition,
348
+ deps.viewTransitionDefault,
349
+ entry.shortCode,
350
+ ),
346
351
  params,
347
352
  belongsToRoute: true,
348
353
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
@@ -366,6 +371,9 @@ export async function resolveOrphanLayout<TEnv>(
366
371
  deps: SegmentResolutionDeps<TEnv>,
367
372
  options?: ResolveSegmentOptions,
368
373
  routeKey?: string,
374
+ /** Parent route entry — its loaders are inherited by the layout so
375
+ * parallel slots inside this layout can access them via useLoader(). */
376
+ parentRouteEntry?: EntryData,
369
377
  ): Promise<ResolvedSegment[]> {
370
378
  invariant(
371
379
  orphan.type === "layout" || orphan.type === "cache",
@@ -381,6 +389,30 @@ export async function resolveOrphanLayout<TEnv>(
381
389
  deps,
382
390
  );
383
391
  segments.push(...loaderSegments);
392
+
393
+ // Inherit parent route's loaders so parallel slots inside this layout
394
+ // can access them via useLoader(). Without this, the route's loaders
395
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
396
+ // which is a child — not a parent — of the layout's context.
397
+ if (
398
+ parentRouteEntry &&
399
+ parentRouteEntry.loader &&
400
+ parentRouteEntry.loader.length > 0 &&
401
+ Object.keys(orphan.parallel).length > 0
402
+ ) {
403
+ const inheritedLoaders = await resolveLoaders(
404
+ parentRouteEntry,
405
+ context,
406
+ belongsToRoute,
407
+ deps,
408
+ orphan.shortCode,
409
+ );
410
+ // Tag as inherited so buildMatchResult can deduplicate when safe
411
+ for (const s of inheritedLoaders) {
412
+ s._inherited = true;
413
+ }
414
+ segments.push(...inheritedLoaders);
415
+ }
384
416
  }
385
417
 
386
418
  // Handler-first: orphan layout handler executes before its parallels
@@ -399,7 +431,11 @@ export async function resolveOrphanLayout<TEnv>(
399
431
  belongsToRoute,
400
432
  layoutName: orphan.id,
401
433
  loading: orphan.loading === false ? null : orphan.loading,
402
- transition: orphan.transition,
434
+ transition: applyViewTransitionDefault(
435
+ orphan.transition,
436
+ deps.viewTransitionDefault,
437
+ orphan.shortCode,
438
+ ),
403
439
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
404
440
  });
405
441
 
@@ -471,6 +507,14 @@ export async function resolveParallelEntry<TEnv>(
471
507
  if (handler === undefined) {
472
508
  continue;
473
509
  }
510
+ // Pin `_currentSegmentId` to the slot's own id so handle pushes from
511
+ // inside the slot handler get their own bucket in the HandleStore.
512
+ // Parent-keying would collapse them into the parent layout's bucket;
513
+ // the partial-update merge then replaces the parent's bucket on a
514
+ // slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
515
+ // filterSegmentOrder() retains slot ids so the client preserves them.
516
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
517
+ `${parentShortCode}.${slot}`;
474
518
  const doneParallelHandler = track(
475
519
  `handler:${parallelEntry.id}.${slot}`,
476
520
  2,
@@ -479,7 +523,9 @@ export async function resolveParallelEntry<TEnv>(
479
523
  parallelEntry.loading !== undefined && parallelEntry.loading !== false;
480
524
  if (hasLoadingFallback) {
481
525
  const result =
482
- typeof handler === "function" ? handler(context) : handler;
526
+ typeof handler === "function"
527
+ ? observeHandler(`${parallelEntry.id}.${slot}`, handler, context)
528
+ : handler;
483
529
  if (result instanceof Promise) {
484
530
  result.finally(doneParallelHandler).catch(() => {});
485
531
  const tracked = deps.trackHandler(result, {
@@ -501,7 +547,13 @@ export async function resolveParallelEntry<TEnv>(
501
547
  }
502
548
  } else {
503
549
  component =
504
- typeof handler === "function" ? await handler(context) : handler;
550
+ typeof handler === "function"
551
+ ? await observeHandler(
552
+ `${parallelEntry.id}.${slot}`,
553
+ handler,
554
+ context,
555
+ )
556
+ : handler;
505
557
  doneParallelHandler();
506
558
  }
507
559
  }
@@ -513,7 +565,11 @@ export async function resolveParallelEntry<TEnv>(
513
565
  index: 0,
514
566
  component,
515
567
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
516
- transition: parallelEntry.transition,
568
+ transition: applyViewTransitionDefault(
569
+ parallelEntry.transition,
570
+ deps.viewTransitionDefault,
571
+ `${parentShortCode}.${slot}`,
572
+ ),
517
573
  params,
518
574
  slot,
519
575
  belongsToRoute,
@@ -576,6 +632,13 @@ export async function resolveAllSegments<TEnv>(
576
632
  } catch {}
577
633
 
578
634
  for (const entry of entries) {
635
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
636
+ // can guard non-cacheable variable reads. Also guards response-level
637
+ // side effects (headers.set). Persists for all descendant entries.
638
+ if (entry.type === "cache") {
639
+ const store = RangoContext.getStore();
640
+ if (store) store.insideCacheScope = true;
641
+ }
579
642
  const doneEntry = track(`segment:${entry.id}`, 1);
580
643
  const resolvedSegments = await resolveWithErrorBoundary(
581
644
  entry,
@@ -595,6 +658,7 @@ export async function resolveAllSegments<TEnv>(
595
658
  deps,
596
659
  { request: safeRequest, url: context.url, routeKey, telemetry },
597
660
  context.pathname,
661
+ options?.throwOnError,
598
662
  );
599
663
  doneEntry();
600
664
  // Deduplicate by segment ID. include() scopes can produce entries that
@@ -620,20 +684,37 @@ export async function resolveLoadersOnly<TEnv>(
620
684
  deps: SegmentResolutionDeps<TEnv>,
621
685
  ): Promise<ResolvedSegment[]> {
622
686
  const loaderSegments: ResolvedSegment[] = [];
687
+ const seenIds = new Set<string>();
623
688
 
624
689
  async function collectEntryLoaders(
625
690
  entry: EntryData,
626
691
  belongsToRoute: boolean,
627
692
  shortCodeOverride?: string,
628
693
  ): Promise<void> {
629
- const segments = await resolveLoaders(
630
- entry,
631
- context,
632
- belongsToRoute,
633
- deps,
634
- shortCodeOverride,
635
- );
636
- loaderSegments.push(...segments);
694
+ // Skip if all loaders from this entry have already been resolved
695
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
696
+ const entryLoaders = entry.loader ?? [];
697
+ const sc = shortCodeOverride ?? entry.shortCode;
698
+ const allAlreadySeen =
699
+ entryLoaders.length > 0 &&
700
+ entryLoaders.every((le, i) =>
701
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
702
+ );
703
+ if (!allAlreadySeen) {
704
+ const segments = await resolveLoaders(
705
+ entry,
706
+ context,
707
+ belongsToRoute,
708
+ deps,
709
+ shortCodeOverride,
710
+ );
711
+ for (const seg of segments) {
712
+ if (!seenIds.has(seg.id)) {
713
+ seenIds.add(seg.id);
714
+ loaderSegments.push(seg);
715
+ }
716
+ }
717
+ }
637
718
 
638
719
  const seenParallelEntryIds = new Set<string>();
639
720
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -645,6 +726,30 @@ export async function resolveLoadersOnly<TEnv>(
645
726
  const childBelongsToRoute = belongsToRoute || entry.type === "route";
646
727
  for (const layoutEntry of entry.layout) {
647
728
  await collectEntryLoaders(layoutEntry, childBelongsToRoute);
729
+ // Inherit route loaders for orphan layouts with parallels.
730
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
731
+ // route entry, as that would re-iterate route.layout and loop.
732
+ if (
733
+ entry.type === "route" &&
734
+ entry.loader &&
735
+ entry.loader.length > 0 &&
736
+ Object.keys(layoutEntry.parallel).length > 0
737
+ ) {
738
+ const inherited = await resolveLoaders(
739
+ entry,
740
+ context,
741
+ childBelongsToRoute,
742
+ deps,
743
+ layoutEntry.shortCode,
744
+ );
745
+ for (const seg of inherited) {
746
+ if (!seenIds.has(seg.id)) {
747
+ seenIds.add(seg.id);
748
+ seg._inherited = true;
749
+ loaderSegments.push(seg);
750
+ }
751
+ }
752
+ }
648
753
  }
649
754
  }
650
755
 
@@ -8,7 +8,7 @@
8
8
  * - Error boundary segment creation
9
9
  */
10
10
 
11
- import type { ReactNode } from "react";
11
+ import { createElement, type ReactNode } from "react";
12
12
  import { DataNotFoundError } from "../../errors";
13
13
  import {
14
14
  createErrorInfo,
@@ -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
  // ---------------------------------------------------------------------------
@@ -180,34 +254,39 @@ export function catchSegmentError<TEnv>(
180
254
 
181
255
  if (error instanceof DataNotFoundError) {
182
256
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
257
+ // Fall back to router's notFound component, then a plain default
258
+ const notFoundOption = deps.notFoundComponent;
259
+ const defaultFallback =
260
+ typeof notFoundOption === "function"
261
+ ? notFoundOption({ pathname: pathname ?? "" })
262
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
263
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
264
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
265
+ const notFoundInfo = createNotFoundInfo(
266
+ error,
267
+ entry.shortCode,
268
+ entry.type,
269
+ pathname,
270
+ );
191
271
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
272
+ reportError(true, {
273
+ notFound: true,
274
+ message: notFoundInfo.message,
275
+ });
196
276
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
277
+ debugLog("segment", "notFound boundary handled error", {
278
+ segmentId: entry.shortCode,
279
+ message: notFoundInfo.message,
280
+ });
201
281
 
202
- setResponseStatus(404);
282
+ setResponseStatus(404);
203
283
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
284
+ return createNotFoundSegment(
285
+ notFoundInfo,
286
+ effectiveNotFoundFallback,
287
+ entry,
288
+ params,
289
+ );
211
290
  }
212
291
 
213
292
  const fallback = deps.findNearestErrorBoundary(entry);
@@ -245,11 +324,17 @@ export async function resolveWithErrorBoundary<TEnv, TResult>(
245
324
  deps: SegmentResolutionDeps<TEnv>,
246
325
  report?: ErrorReportContext,
247
326
  pathname?: string,
327
+ throwOnError?: boolean,
248
328
  ): Promise<TResult> {
249
329
  try {
250
330
  return await resolveFn();
251
331
  } catch (error) {
252
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;
253
338
  const segment = catchSegmentError(
254
339
  error,
255
340
  entry,