@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +120 -204
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +190 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +63 -24
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +338 -126
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { ReactNode } from "react";
9
9
  import { invariant } from "../../errors";
10
- import type { EntryData } from "../../server/context";
10
+ import {
11
+ getParallelEntries,
12
+ getParallelSlotEntries,
13
+ type EntryData,
14
+ } from "../../server/context";
11
15
  import type {
12
16
  HandlerContext,
13
17
  InternalHandlerContext,
@@ -15,59 +19,24 @@ import type {
15
19
  } from "../../types";
16
20
  import type { SegmentResolutionDeps } from "../types.js";
17
21
  import { resolveLoaderData } from "./loader-cache.js";
22
+ import { _getRequestContext } from "../../server/request-context.js";
23
+ import { appendMetric } from "../metrics.js";
18
24
  import {
19
25
  handleHandlerResult,
20
26
  tryStaticHandler,
21
27
  tryStaticSlot,
22
28
  resolveLayoutComponent,
23
29
  resolveWithErrorBoundary,
30
+ warnOnStreamedResponse,
24
31
  } from "./helpers.js";
32
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
25
33
  import { getRouterContext } from "../router-context.js";
26
- import { resolveSink, safeEmit } from "../telemetry.js";
27
- import { track } from "../../server/context.js";
28
-
29
- // ---------------------------------------------------------------------------
30
- // Streamed handler telemetry
31
- // ---------------------------------------------------------------------------
32
-
33
- /**
34
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
35
- * React catches the actual error via its error boundary; this only emits
36
- * the handler.error telemetry event.
37
- */
38
- function observeStreamedHandler(
39
- promise: Promise<ReactNode>,
40
- segmentId: string,
41
- segmentType: string,
42
- pathname?: string,
43
- routeKey?: string,
44
- params?: Record<string, string>,
45
- ): void {
46
- let routerCtx;
47
- try {
48
- routerCtx = getRouterContext();
49
- } catch {
50
- return;
51
- }
52
- if (!routerCtx?.telemetry) return;
53
- const sink = resolveSink(routerCtx.telemetry);
54
- const reqId = routerCtx.requestId;
55
- promise.catch((err: unknown) => {
56
- const errorObj = err instanceof Error ? err : new Error(String(err));
57
- safeEmit(sink, {
58
- type: "handler.error",
59
- timestamp: performance.now(),
60
- requestId: reqId,
61
- segmentId,
62
- segmentType,
63
- error: errorObj,
64
- handledByBoundary: true,
65
- pathname,
66
- routeKey,
67
- params,
68
- });
69
- });
70
- }
34
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
35
+ import {
36
+ track,
37
+ RangoContext,
38
+ runInsideLoaderScope,
39
+ } from "../../server/context.js";
71
40
 
72
41
  // ---------------------------------------------------------------------------
73
42
  // Fresh path (full match, no revalidation)
@@ -90,9 +59,11 @@ export async function resolveLoaders<TEnv>(
90
59
  const shortCode = shortCodeOverride ?? entry.shortCode;
91
60
  const hasLoading = "loading" in entry && entry.loading !== undefined;
92
61
  const loadingDisabled = hasLoading && entry.loading === false;
62
+ const ms = _getRequestContext()?._metricsStore;
93
63
 
94
64
  if (!loadingDisabled) {
95
- return loaderEntries.map((loaderEntry, i) => {
65
+ // Streaming loaders: promises kick off now, settle during RSC serialization.
66
+ const segments = loaderEntries.map((loaderEntry, i) => {
96
67
  const { loader } = loaderEntry;
97
68
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
98
69
  return {
@@ -104,7 +75,9 @@ export async function resolveLoaders<TEnv>(
104
75
  params: ctx.params,
105
76
  loaderId: loader.$$id,
106
77
  loaderData: deps.wrapLoaderPromise(
107
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
78
+ runInsideLoaderScope(() =>
79
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
80
+ ),
108
81
  entry,
109
82
  segmentId,
110
83
  ctx.pathname,
@@ -112,32 +85,61 @@ export async function resolveLoaders<TEnv>(
112
85
  belongsToRoute,
113
86
  };
114
87
  });
88
+
89
+ return segments;
115
90
  }
116
91
 
117
92
  // Loading disabled: still start all loaders in parallel, but only emit
118
93
  // settled promises so handlers don't stream loading placeholders.
119
- const pendingLoaderData = loaderEntries.map((loaderEntry) =>
120
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
121
- );
122
- await Promise.all(pendingLoaderData);
94
+ //
95
+ // Wrap each loader promise with wrapLoaderPromise BEFORE awaiting. The wrapped
96
+ // promise resolves to a LoaderDataResult and never rejects, routing a failed
97
+ // loader to its own per-loader error boundary. Awaiting the RAW promises here
98
+ // instead would (1) propagate a rejection to the segment-level boundary,
99
+ // collapsing the whole entry and discarding successful sibling data, and
100
+ // (2) leave the other in-flight raw promises without a .catch, producing
101
+ // unhandled rejections. Mirrors the loading path and intercept-resolution.
102
+ const pendingLoaderData = loaderEntries.map((loaderEntry, i) => {
103
+ const { loader } = loaderEntry;
104
+ const segmentId = `${shortCode}D${i}.${loader.$$id}`;
105
+ const start = performance.now();
106
+ const wrapped = deps.wrapLoaderPromise(
107
+ runInsideLoaderScope(() =>
108
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
109
+ ),
110
+ entry,
111
+ segmentId,
112
+ ctx.pathname,
113
+ );
114
+ return { wrapped, start, segmentId, loaderId: loader.$$id };
115
+ });
116
+ await Promise.all(pendingLoaderData.map((p) => p.wrapped));
123
117
 
124
118
  return loaderEntries.map((loaderEntry, i) => {
125
119
  const { loader } = loaderEntry;
126
- const segmentId = `${shortCode}D${i}.${loader.$$id}`;
120
+ const pending = pendingLoaderData[i]!;
121
+ if (ms && !ms.metrics.some((m) => m.label === `loader:${loader.$$id}`)) {
122
+ // All loaders ran in parallel via Promise.all — each span covers
123
+ // from its own kickoff to the batch settlement, giving a ceiling
124
+ // on that loader's contribution to the overall wait.
125
+ const batchEnd = performance.now();
126
+ appendMetric(
127
+ ms,
128
+ `loader:${loader.$$id}`,
129
+ pending.start,
130
+ batchEnd - pending.start,
131
+ 2,
132
+ );
133
+ }
127
134
  return {
128
- id: segmentId,
135
+ id: pending.segmentId,
129
136
  namespace: entry.id,
130
137
  type: "loader" as const,
131
138
  index: i,
132
139
  component: null,
133
140
  params: ctx.params,
134
141
  loaderId: loader.$$id,
135
- loaderData: deps.wrapLoaderPromise(
136
- pendingLoaderData[i]!,
137
- entry,
138
- segmentId,
139
- ctx.pathname,
140
- ),
142
+ loaderData: pending.wrapped,
141
143
  belongsToRoute,
142
144
  };
143
145
  });
@@ -190,14 +192,20 @@ export async function resolveSegment<TEnv>(
190
192
  index: 0,
191
193
  component,
192
194
  loading: entry.loading === false ? null : entry.loading,
193
- transition: entry.transition,
195
+ transition: applyViewTransitionDefault(
196
+ entry.transition,
197
+ deps.viewTransitionDefault,
198
+ ),
194
199
  params,
195
200
  belongsToRoute: false,
196
201
  layoutName: entry.id,
197
202
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
198
203
  });
199
204
 
200
- for (const parallelEntry of entry.parallel) {
205
+ const resolvedParallelEntries = new Set<string>();
206
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
207
+ entry.parallel,
208
+ )) {
201
209
  const parallelSegments = await resolveParallelEntry(
202
210
  parallelEntry,
203
211
  params,
@@ -207,8 +215,11 @@ export async function resolveSegment<TEnv>(
207
215
  deps,
208
216
  options,
209
217
  routeKey,
218
+ [slot],
219
+ !resolvedParallelEntries.has(parallelEntry.id),
210
220
  );
211
221
  segments.push(...parallelSegments);
222
+ resolvedParallelEntries.add(parallelEntry.id);
212
223
  }
213
224
 
214
225
  for (const orphan of entry.layout) {
@@ -244,10 +255,16 @@ export async function resolveSegment<TEnv>(
244
255
  entry.shortCode,
245
256
  );
246
257
  if (component === undefined) {
258
+ // For Passthrough routes at runtime, use the live handler instead of
259
+ // the build handler. At build time (context.build === true), always
260
+ // use the build handler from entry.handler.
261
+ const handler =
262
+ !context.build && entry.liveHandler ? entry.liveHandler : entry.handler;
247
263
  const doneRouteHandler = track(`handler:${entry.id}`, 2);
248
264
  if (entry.loading) {
249
- const result = handleHandlerResult(entry.handler(context));
265
+ const result = handleHandlerResult(handler(context));
250
266
  if (result instanceof Promise) {
267
+ warnOnStreamedResponse(result, entry.id);
251
268
  result.finally(doneRouteHandler).catch(() => {});
252
269
  const tracked = deps.trackHandler(result, {
253
270
  segmentId: entry.shortCode,
@@ -267,7 +284,7 @@ export async function resolveSegment<TEnv>(
267
284
  component = result;
268
285
  }
269
286
  } else {
270
- component = handleHandlerResult(await entry.handler(context));
287
+ component = handleHandlerResult(await handler(context));
271
288
  doneRouteHandler();
272
289
  }
273
290
  }
@@ -282,11 +299,15 @@ export async function resolveSegment<TEnv>(
282
299
  deps,
283
300
  options,
284
301
  routeKey,
302
+ entry,
285
303
  );
286
304
  segments.push(...orphanSegments);
287
305
  }
288
306
 
289
- for (const parallelEntry of entry.parallel) {
307
+ const resolvedParallelEntries = new Set<string>();
308
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
309
+ entry.parallel,
310
+ )) {
290
311
  const parallelSegments = await resolveParallelEntry(
291
312
  parallelEntry,
292
313
  params,
@@ -296,8 +317,11 @@ export async function resolveSegment<TEnv>(
296
317
  deps,
297
318
  options,
298
319
  routeKey,
320
+ [slot],
321
+ !resolvedParallelEntries.has(parallelEntry.id),
299
322
  );
300
323
  segments.push(...parallelSegments);
324
+ resolvedParallelEntries.add(parallelEntry.id);
301
325
  }
302
326
 
303
327
  segments.push({
@@ -305,9 +329,12 @@ export async function resolveSegment<TEnv>(
305
329
  namespace: entry.id,
306
330
  type: "route",
307
331
  index: 0,
308
- component,
332
+ component: component ?? null,
309
333
  loading: entry.loading === false ? null : entry.loading,
310
- transition: entry.transition,
334
+ transition: applyViewTransitionDefault(
335
+ entry.transition,
336
+ deps.viewTransitionDefault,
337
+ ),
311
338
  params,
312
339
  belongsToRoute: true,
313
340
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
@@ -331,6 +358,9 @@ export async function resolveOrphanLayout<TEnv>(
331
358
  deps: SegmentResolutionDeps<TEnv>,
332
359
  options?: ResolveSegmentOptions,
333
360
  routeKey?: string,
361
+ /** Parent route entry — its loaders are inherited by the layout so
362
+ * parallel slots inside this layout can access them via useLoader(). */
363
+ parentRouteEntry?: EntryData,
334
364
  ): Promise<ResolvedSegment[]> {
335
365
  invariant(
336
366
  orphan.type === "layout" || orphan.type === "cache",
@@ -346,6 +376,30 @@ export async function resolveOrphanLayout<TEnv>(
346
376
  deps,
347
377
  );
348
378
  segments.push(...loaderSegments);
379
+
380
+ // Inherit parent route's loaders so parallel slots inside this layout
381
+ // can access them via useLoader(). Without this, the route's loaders
382
+ // are only in the route's OutletProvider (rendered as <Outlet /> content),
383
+ // which is a child — not a parent — of the layout's context.
384
+ if (
385
+ parentRouteEntry &&
386
+ parentRouteEntry.loader &&
387
+ parentRouteEntry.loader.length > 0 &&
388
+ Object.keys(orphan.parallel).length > 0
389
+ ) {
390
+ const inheritedLoaders = await resolveLoaders(
391
+ parentRouteEntry,
392
+ context,
393
+ belongsToRoute,
394
+ deps,
395
+ orphan.shortCode,
396
+ );
397
+ // Tag as inherited so buildMatchResult can deduplicate when safe
398
+ for (const s of inheritedLoaders) {
399
+ s._inherited = true;
400
+ }
401
+ segments.push(...inheritedLoaders);
402
+ }
349
403
  }
350
404
 
351
405
  // Handler-first: orphan layout handler executes before its parallels
@@ -364,11 +418,17 @@ export async function resolveOrphanLayout<TEnv>(
364
418
  belongsToRoute,
365
419
  layoutName: orphan.id,
366
420
  loading: orphan.loading === false ? null : orphan.loading,
367
- transition: orphan.transition,
421
+ transition: applyViewTransitionDefault(
422
+ orphan.transition,
423
+ deps.viewTransitionDefault,
424
+ ),
368
425
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
369
426
  });
370
427
 
371
- for (const parallelEntry of orphan.parallel) {
428
+ const resolvedParallelEntries = new Set<string>();
429
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
430
+ orphan.parallel,
431
+ )) {
372
432
  const parallelSegments = await resolveParallelEntry(
373
433
  parallelEntry,
374
434
  params,
@@ -378,8 +438,11 @@ export async function resolveOrphanLayout<TEnv>(
378
438
  deps,
379
439
  options,
380
440
  routeKey,
441
+ [slot],
442
+ !resolvedParallelEntries.has(parallelEntry.id),
381
443
  );
382
444
  segments.push(...parallelSegments);
445
+ resolvedParallelEntries.add(parallelEntry.id);
383
446
  }
384
447
 
385
448
  return segments;
@@ -397,6 +460,8 @@ export async function resolveParallelEntry<TEnv>(
397
460
  deps: SegmentResolutionDeps<TEnv>,
398
461
  options?: ResolveSegmentOptions,
399
462
  routeKey?: string,
463
+ slotNames?: `@${string}`[],
464
+ includeLoaders: boolean = true,
400
465
  ): Promise<ResolvedSegment[]> {
401
466
  invariant(
402
467
  parallelEntry.type === "parallel",
@@ -411,7 +476,12 @@ export async function resolveParallelEntry<TEnv>(
411
476
  | ReactNode
412
477
  >;
413
478
 
414
- for (const [slot, handler] of Object.entries(slots)) {
479
+ const slotsToResolve = slotNames ?? (Object.keys(slots) as `@${string}`[]);
480
+
481
+ for (const slot of slotsToResolve) {
482
+ // Try static lookup first — in production, handler bodies are evicted
483
+ // and replaced with stubs that have no .handler property (undefined).
484
+ // The static store holds the pre-rendered component for these slots.
415
485
  let component: ReactNode | undefined = await tryStaticSlot(
416
486
  parallelEntry,
417
487
  slot,
@@ -419,6 +489,18 @@ export async function resolveParallelEntry<TEnv>(
419
489
  );
420
490
 
421
491
  if (component === undefined) {
492
+ const handler = slots[slot];
493
+ if (handler === undefined) {
494
+ continue;
495
+ }
496
+ // Pin `_currentSegmentId` to the slot's own id so handle pushes from
497
+ // inside the slot handler get their own bucket in the HandleStore.
498
+ // Parent-keying would collapse them into the parent layout's bucket;
499
+ // the partial-update merge then replaces the parent's bucket on a
500
+ // slot-only revalidation and drops layout-pushed Meta/Breadcrumbs.
501
+ // filterSegmentOrder() retains slot ids so the client preserves them.
502
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
503
+ `${parentShortCode}.${slot}`;
422
504
  const doneParallelHandler = track(
423
505
  `handler:${parallelEntry.id}.${slot}`,
424
506
  2,
@@ -461,7 +543,10 @@ export async function resolveParallelEntry<TEnv>(
461
543
  index: 0,
462
544
  component,
463
545
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
464
- transition: parallelEntry.transition,
546
+ transition: applyViewTransitionDefault(
547
+ parallelEntry.transition,
548
+ deps.viewTransitionDefault,
549
+ ),
465
550
  params,
466
551
  slot,
467
552
  belongsToRoute,
@@ -472,7 +557,7 @@ export async function resolveParallelEntry<TEnv>(
472
557
  });
473
558
  }
474
559
 
475
- if (!parallelEntry.loading && !options?.skipLoaders) {
560
+ if (!options?.skipLoaders && includeLoaders) {
476
561
  const loaderSegments = await resolveLoaders(
477
562
  parallelEntry,
478
563
  context,
@@ -480,6 +565,15 @@ export async function resolveParallelEntry<TEnv>(
480
565
  deps,
481
566
  parentShortCode,
482
567
  );
568
+ // Tag parallel-owned loaders so renderSegments can stream them
569
+ // using the parallel's loading() instead of awaiting on the layout
570
+ const parallelLoading =
571
+ parallelEntry.loading === false ? undefined : parallelEntry.loading;
572
+ if (parallelLoading) {
573
+ for (const seg of loaderSegments) {
574
+ seg.parallelLoading = parallelLoading;
575
+ }
576
+ }
483
577
  segments.push(...loaderSegments);
484
578
  }
485
579
 
@@ -515,6 +609,13 @@ export async function resolveAllSegments<TEnv>(
515
609
  } catch {}
516
610
 
517
611
  for (const entry of entries) {
612
+ // Set ALS flag when entering a cache() boundary so that ctx.get()
613
+ // can guard non-cacheable variable reads. Also guards response-level
614
+ // side effects (headers.set). Persists for all descendant entries.
615
+ if (entry.type === "cache") {
616
+ const store = RangoContext.getStore();
617
+ if (store) store.insideCacheScope = true;
618
+ }
518
619
  const doneEntry = track(`segment:${entry.id}`, 1);
519
620
  const resolvedSegments = await resolveWithErrorBoundary(
520
621
  entry,
@@ -559,11 +660,77 @@ export async function resolveLoadersOnly<TEnv>(
559
660
  deps: SegmentResolutionDeps<TEnv>,
560
661
  ): Promise<ResolvedSegment[]> {
561
662
  const loaderSegments: ResolvedSegment[] = [];
663
+ const seenIds = new Set<string>();
664
+
665
+ async function collectEntryLoaders(
666
+ entry: EntryData,
667
+ belongsToRoute: boolean,
668
+ shortCodeOverride?: string,
669
+ ): Promise<void> {
670
+ // Skip if all loaders from this entry have already been resolved
671
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
672
+ const entryLoaders = entry.loader ?? [];
673
+ const sc = shortCodeOverride ?? entry.shortCode;
674
+ const allAlreadySeen =
675
+ entryLoaders.length > 0 &&
676
+ entryLoaders.every((le, i) =>
677
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
678
+ );
679
+ if (!allAlreadySeen) {
680
+ const segments = await resolveLoaders(
681
+ entry,
682
+ context,
683
+ belongsToRoute,
684
+ deps,
685
+ shortCodeOverride,
686
+ );
687
+ for (const seg of segments) {
688
+ if (!seenIds.has(seg.id)) {
689
+ seenIds.add(seg.id);
690
+ loaderSegments.push(seg);
691
+ }
692
+ }
693
+ }
694
+
695
+ const seenParallelEntryIds = new Set<string>();
696
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
697
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
698
+ seenParallelEntryIds.add(parallelEntry.id);
699
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
700
+ }
701
+
702
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
703
+ for (const layoutEntry of entry.layout) {
704
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
705
+ // Inherit route loaders for orphan layouts with parallels.
706
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
707
+ // route entry, as that would re-iterate route.layout and loop.
708
+ if (
709
+ entry.type === "route" &&
710
+ entry.loader &&
711
+ entry.loader.length > 0 &&
712
+ Object.keys(layoutEntry.parallel).length > 0
713
+ ) {
714
+ const inherited = await resolveLoaders(
715
+ entry,
716
+ context,
717
+ childBelongsToRoute,
718
+ deps,
719
+ layoutEntry.shortCode,
720
+ );
721
+ for (const seg of inherited) {
722
+ if (!seenIds.has(seg.id)) {
723
+ seenIds.add(seg.id);
724
+ seg._inherited = true;
725
+ loaderSegments.push(seg);
726
+ }
727
+ }
728
+ }
729
+ }
730
+ }
562
731
 
563
732
  for (const entry of entries) {
564
- const belongsToRoute = entry.type === "route";
565
- const segments = await resolveLoaders(entry, context, belongsToRoute, deps);
566
- loaderSegments.push(...segments);
733
+ await collectEntryLoaders(entry, entry.type === "route");
567
734
  }
568
735
 
569
736
  return loaderSegments;
@@ -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,
@@ -52,6 +52,40 @@ export function handleHandlerResult(
52
52
  return result;
53
53
  }
54
54
 
55
+ /**
56
+ * Dev-only: warn when a handler on a route that declares loading() resolves or
57
+ * rejects with a Response (e.g. redirect()).
58
+ *
59
+ * On a non-loading route a returned/thrown Response short-circuits to an HTTP
60
+ * redirect. But when the route declares loading(), the handler result is
61
+ * streamed (not awaited at the resolution boundary), so the Response surfaces
62
+ * only during RSC serialization and is rendered into the stream instead of
63
+ * becoming a 302/308 — a silent failure mode. Issue redirects from middleware,
64
+ * a loader, or a synchronous handler return instead. Compiled out in production.
65
+ */
66
+ export function warnOnStreamedResponse(
67
+ result: Promise<unknown>,
68
+ entryId: string,
69
+ ): void {
70
+ if (process.env.NODE_ENV === "production") return;
71
+ // A Response can surface either as a rejection (handleHandlerResult rethrows a
72
+ // resolved Response) or as a resolved value (the raw parallel-slot handler is
73
+ // not run through handleHandlerResult). Check both so every streamed path is
74
+ // covered. Each handler is an independent observer; it does not consume the
75
+ // rejection for the trackHandler/observeStreamedHandler chains.
76
+ const check = (value: unknown) => {
77
+ if (value instanceof Response) {
78
+ console.warn(
79
+ `[rango] Handler for "${entryId}" returned a Response (e.g. ` +
80
+ `redirect()), but it declares loading(): the Response is rendered ` +
81
+ `into the RSC stream, NOT sent as an HTTP redirect. Issue redirects ` +
82
+ `from middleware, a loader, or a synchronous handler return.`,
83
+ );
84
+ }
85
+ };
86
+ result.then(check, check);
87
+ }
88
+
55
89
  // ---------------------------------------------------------------------------
56
90
  // Static handler interception
57
91
  // ---------------------------------------------------------------------------
@@ -180,34 +214,39 @@ export function catchSegmentError<TEnv>(
180
214
 
181
215
  if (error instanceof DataNotFoundError) {
182
216
  const notFoundFallback = deps.findNearestNotFoundBoundary(entry);
217
+ // Fall back to router's notFound component, then a plain default
218
+ const notFoundOption = deps.notFoundComponent;
219
+ const defaultFallback =
220
+ typeof notFoundOption === "function"
221
+ ? notFoundOption({ pathname: pathname ?? "" })
222
+ : (notFoundOption ?? createElement("h1", null, "Not Found"));
223
+ const effectiveNotFoundFallback = notFoundFallback ?? defaultFallback;
183
224
 
184
- if (notFoundFallback) {
185
- const notFoundInfo = createNotFoundInfo(
186
- error,
187
- entry.shortCode,
188
- entry.type,
189
- pathname,
190
- );
225
+ const notFoundInfo = createNotFoundInfo(
226
+ error,
227
+ entry.shortCode,
228
+ entry.type,
229
+ pathname,
230
+ );
191
231
 
192
- reportError(true, {
193
- notFound: true,
194
- message: notFoundInfo.message,
195
- });
232
+ reportError(true, {
233
+ notFound: true,
234
+ message: notFoundInfo.message,
235
+ });
196
236
 
197
- debugLog("segment", "notFound boundary handled error", {
198
- segmentId: entry.shortCode,
199
- message: notFoundInfo.message,
200
- });
237
+ debugLog("segment", "notFound boundary handled error", {
238
+ segmentId: entry.shortCode,
239
+ message: notFoundInfo.message,
240
+ });
201
241
 
202
- setResponseStatus(404);
242
+ setResponseStatus(404);
203
243
 
204
- return createNotFoundSegment(
205
- notFoundInfo,
206
- notFoundFallback,
207
- entry,
208
- params,
209
- );
210
- }
244
+ return createNotFoundSegment(
245
+ notFoundInfo,
246
+ effectiveNotFoundFallback,
247
+ entry,
248
+ params,
249
+ );
211
250
  }
212
251
 
213
252
  const fallback = deps.findNearestErrorBoundary(entry);