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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +303 -741
  3. package/dist/bin/rango.js +730 -184
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +4344 -1335
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +86 -15
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +85 -6
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +251 -24
  13. package/skills/caching/SKILL.md +375 -17
  14. package/skills/catalog.json +271 -0
  15. package/skills/comparison/SKILL.md +50 -0
  16. package/skills/comparison/agents/openai.yaml +4 -0
  17. package/skills/comparison/references/framework-comparison.md +837 -0
  18. package/skills/composability/SKILL.md +110 -4
  19. package/skills/css/SKILL.md +76 -0
  20. package/skills/debug-manifest/SKILL.md +5 -3
  21. package/skills/defer-hydration/SKILL.md +235 -0
  22. package/skills/document-cache/SKILL.md +87 -56
  23. package/skills/fonts/SKILL.md +1 -1
  24. package/skills/handler-use/SKILL.md +364 -0
  25. package/skills/hooks/SKILL.md +73 -691
  26. package/skills/hooks/data.md +273 -0
  27. package/skills/hooks/handle-and-actions.md +103 -0
  28. package/skills/hooks/navigation.md +110 -0
  29. package/skills/hooks/outlets.md +41 -0
  30. package/skills/hooks/state.md +228 -0
  31. package/skills/hooks/urls.md +135 -0
  32. package/skills/host-router/SKILL.md +129 -27
  33. package/skills/i18n/SKILL.md +276 -0
  34. package/skills/intercept/SKILL.md +94 -18
  35. package/skills/layout/SKILL.md +62 -19
  36. package/skills/links/SKILL.md +249 -17
  37. package/skills/loader/SKILL.md +302 -54
  38. package/skills/middleware/SKILL.md +59 -16
  39. package/skills/migrate-nextjs/SKILL.md +745 -0
  40. package/skills/migrate-react-router/SKILL.md +153 -0
  41. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  42. package/skills/migrate-react-router/component-migration.md +196 -0
  43. package/skills/migrate-react-router/data-and-actions.md +225 -0
  44. package/skills/migrate-react-router/route-mapping.md +271 -0
  45. package/skills/mime-routes/SKILL.md +29 -2
  46. package/skills/observability/SKILL.md +202 -0
  47. package/skills/parallel/SKILL.md +225 -10
  48. package/skills/ppr/SKILL.md +622 -0
  49. package/skills/prerender/SKILL.md +178 -124
  50. package/skills/rango/SKILL.md +318 -24
  51. package/skills/react-compiler/SKILL.md +168 -0
  52. package/skills/response-routes/SKILL.md +138 -49
  53. package/skills/route/SKILL.md +172 -9
  54. package/skills/router-setup/SKILL.md +131 -11
  55. package/skills/scripts/SKILL.md +179 -0
  56. package/skills/server-actions/SKILL.md +776 -0
  57. package/skills/shell-manifest/SKILL.md +185 -0
  58. package/skills/streams-and-websockets/SKILL.md +283 -0
  59. package/skills/tailwind/SKILL.md +28 -4
  60. package/skills/testing/SKILL.md +130 -0
  61. package/skills/testing/bindings.md +103 -0
  62. package/skills/testing/cache-prerender.md +127 -0
  63. package/skills/testing/client-components.md +124 -0
  64. package/skills/testing/e2e-parity.md +125 -0
  65. package/skills/testing/flight.md +91 -0
  66. package/skills/testing/handles.md +131 -0
  67. package/skills/testing/loader.md +128 -0
  68. package/skills/testing/middleware.md +99 -0
  69. package/skills/testing/render-handler.md +122 -0
  70. package/skills/testing/response-routes.md +95 -0
  71. package/skills/testing/reverse-and-types.md +85 -0
  72. package/skills/testing/server-actions.md +107 -0
  73. package/skills/testing/server-tree.md +128 -0
  74. package/skills/testing/setup.md +123 -0
  75. package/skills/theme/SKILL.md +1 -1
  76. package/skills/typesafety/SKILL.md +45 -616
  77. package/skills/typesafety/env-and-bindings.md +254 -0
  78. package/skills/typesafety/generated-files-and-cli.md +335 -0
  79. package/skills/typesafety/params-and-search.md +153 -0
  80. package/skills/typesafety/route-types.md +209 -0
  81. package/skills/use-cache/SKILL.md +74 -15
  82. package/skills/vercel/SKILL.md +128 -0
  83. package/skills/view-transitions/SKILL.md +337 -0
  84. package/src/__augment-tests__/augment.ts +81 -0
  85. package/src/__augment-tests__/augmented.check.ts +116 -0
  86. package/src/__internal.ts +1 -66
  87. package/src/browser/action-coordinator.ts +53 -36
  88. package/src/browser/action-fence.ts +47 -0
  89. package/src/browser/app-shell.ts +39 -0
  90. package/src/browser/app-version.ts +14 -0
  91. package/src/browser/connection-warmup.ts +134 -0
  92. package/src/browser/cookie-name.ts +140 -0
  93. package/src/browser/event-controller.ts +257 -158
  94. package/src/browser/history-state.ts +21 -0
  95. package/src/browser/index.ts +3 -3
  96. package/src/browser/invalidate-client-cache.ts +52 -0
  97. package/src/browser/logging.ts +28 -0
  98. package/src/browser/merge-segment-loaders.ts +6 -4
  99. package/src/browser/navigation-bridge.ts +132 -33
  100. package/src/browser/navigation-client.ts +218 -68
  101. package/src/browser/navigation-store-handle.ts +38 -0
  102. package/src/browser/navigation-store.ts +203 -80
  103. package/src/browser/navigation-transaction.ts +18 -66
  104. package/src/browser/network-error-handler.ts +34 -7
  105. package/src/browser/partial-update.ts +241 -127
  106. package/src/browser/prefetch/cache.ts +271 -44
  107. package/src/browser/prefetch/fetch.ts +367 -40
  108. package/src/browser/prefetch/queue.ts +144 -23
  109. package/src/browser/prefetch/resource-ready.ts +77 -0
  110. package/src/browser/rango-state.ts +158 -76
  111. package/src/browser/react/Link.tsx +121 -16
  112. package/src/browser/react/NavigationProvider.tsx +240 -122
  113. package/src/browser/react/ScrollRestoration.tsx +10 -6
  114. package/src/browser/react/context.ts +7 -2
  115. package/src/browser/react/filter-segment-order.ts +66 -7
  116. package/src/browser/react/index.ts +0 -48
  117. package/src/browser/react/location-state-shared.ts +178 -8
  118. package/src/browser/react/location-state.ts +39 -14
  119. package/src/browser/react/use-action.ts +6 -15
  120. package/src/browser/react/use-handle.ts +23 -69
  121. package/src/browser/react/use-href.tsx +8 -1
  122. package/src/browser/react/use-link-status.ts +33 -8
  123. package/src/browser/react/use-navigation.ts +32 -7
  124. package/src/browser/react/use-params.ts +20 -10
  125. package/src/browser/react/use-reverse.ts +106 -0
  126. package/src/browser/react/use-router.ts +46 -11
  127. package/src/browser/react/use-search-params.ts +0 -5
  128. package/src/browser/react/use-segments.ts +11 -21
  129. package/src/browser/response-adapter.ts +99 -8
  130. package/src/browser/rsc-router.tsx +272 -80
  131. package/src/browser/scroll-restoration.ts +56 -22
  132. package/src/browser/segment-reconciler.ts +44 -7
  133. package/src/browser/segment-structure-assert.ts +2 -2
  134. package/src/browser/server-action-bridge.ts +244 -71
  135. package/src/browser/types.ts +136 -12
  136. package/src/browser/validate-redirect-origin.ts +43 -16
  137. package/src/build/collect-fallback-refs.ts +107 -0
  138. package/src/build/generate-manifest.ts +207 -158
  139. package/src/build/generate-route-types.ts +6 -1
  140. package/src/build/index.ts +11 -3
  141. package/src/build/prefix-tree-utils.ts +123 -0
  142. package/src/build/route-trie.ts +198 -41
  143. package/src/build/route-types/ast-route-extraction.ts +15 -8
  144. package/src/build/route-types/codegen.ts +16 -5
  145. package/src/build/route-types/include-resolution.ts +464 -63
  146. package/src/build/route-types/param-extraction.ts +6 -3
  147. package/src/build/route-types/per-module-writer.ts +22 -6
  148. package/src/build/route-types/router-processing.ts +336 -110
  149. package/src/build/route-types/scan-filter.ts +9 -2
  150. package/src/build/route-types/source-scan.ts +216 -0
  151. package/src/build/runtime-discovery.ts +13 -21
  152. package/src/cache/cache-error.ts +104 -0
  153. package/src/cache/cache-key-utils.ts +58 -13
  154. package/src/cache/cache-policy.ts +108 -34
  155. package/src/cache/cache-runtime.ts +454 -97
  156. package/src/cache/cache-scope.ts +235 -103
  157. package/src/cache/cache-tag.ts +149 -0
  158. package/src/cache/cf/cf-base64.ts +33 -0
  159. package/src/cache/cf/cf-cache-constants.ts +127 -0
  160. package/src/cache/cf/cf-cache-store.ts +2446 -170
  161. package/src/cache/cf/cf-cache-types.ts +349 -0
  162. package/src/cache/cf/cf-kv-utils.ts +46 -0
  163. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  164. package/src/cache/cf/index.ts +11 -17
  165. package/src/cache/document-cache.ts +144 -49
  166. package/src/cache/handle-snapshot.ts +70 -0
  167. package/src/cache/index.ts +24 -20
  168. package/src/cache/memory-segment-store.ts +243 -37
  169. package/src/cache/profile-registry.ts +46 -31
  170. package/src/cache/read-through-swr.ts +56 -12
  171. package/src/cache/segment-codec.ts +13 -21
  172. package/src/cache/shell-snapshot.ts +417 -0
  173. package/src/cache/tag-invalidation.ts +230 -0
  174. package/src/cache/taint.ts +55 -0
  175. package/src/cache/types.ts +194 -99
  176. package/src/cache/vercel/index.ts +11 -0
  177. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  178. package/src/client.rsc.tsx +41 -21
  179. package/src/client.tsx +116 -290
  180. package/src/cloudflare/index.ts +11 -0
  181. package/src/cloudflare/tracing.ts +108 -0
  182. package/src/component-utils.ts +19 -0
  183. package/src/components/DefaultDocument.tsx +8 -2
  184. package/src/context-var.ts +84 -2
  185. package/src/debug.ts +2 -2
  186. package/src/decode-loader-results.ts +52 -0
  187. package/src/defer.ts +185 -0
  188. package/src/deps/ssr.ts +0 -1
  189. package/src/encode-kv.ts +49 -0
  190. package/src/errors.ts +30 -4
  191. package/src/escape-script.ts +52 -0
  192. package/src/handle.ts +104 -34
  193. package/src/handles/MetaTags.tsx +24 -53
  194. package/src/handles/Scripts.tsx +183 -0
  195. package/src/handles/breadcrumbs.ts +35 -8
  196. package/src/handles/deferred-resolution.ts +127 -0
  197. package/src/handles/is-thenable.ts +18 -0
  198. package/src/handles/meta.ts +14 -40
  199. package/src/handles/script.ts +244 -0
  200. package/src/host/cookie-handler.ts +9 -60
  201. package/src/host/errors.ts +13 -22
  202. package/src/host/index.ts +9 -2
  203. package/src/host/pattern-matcher.ts +23 -52
  204. package/src/host/router.ts +107 -99
  205. package/src/host/testing.ts +40 -27
  206. package/src/host/types.ts +37 -4
  207. package/src/host/utils.ts +1 -1
  208. package/src/href-client.ts +137 -22
  209. package/src/index.rsc.ts +100 -13
  210. package/src/index.ts +143 -19
  211. package/src/internal-debug.ts +11 -10
  212. package/src/loader-store.ts +500 -0
  213. package/src/loader.rsc.ts +20 -13
  214. package/src/loader.ts +12 -11
  215. package/src/missing-id-error.ts +68 -0
  216. package/src/outlet-context.ts +1 -1
  217. package/src/outlet-provider.tsx +1 -5
  218. package/src/prerender/param-hash.ts +16 -16
  219. package/src/prerender/store.ts +37 -41
  220. package/src/prerender.ts +215 -86
  221. package/src/redirect-origin.ts +114 -0
  222. package/src/regex-escape.ts +8 -0
  223. package/src/render-error-thrower.tsx +20 -0
  224. package/src/response-utils.ts +62 -0
  225. package/src/reverse.ts +65 -15
  226. package/src/root-error-boundary.tsx +1 -19
  227. package/src/route-content-wrapper.tsx +19 -77
  228. package/src/route-definition/dsl-helpers.ts +485 -303
  229. package/src/route-definition/helper-factories.ts +28 -140
  230. package/src/route-definition/helpers-types.ts +153 -77
  231. package/src/route-definition/index.ts +4 -2
  232. package/src/route-definition/redirect.ts +53 -12
  233. package/src/route-definition/resolve-handler-use.ts +160 -0
  234. package/src/route-definition/use-item-types.ts +29 -0
  235. package/src/route-map-builder.ts +48 -21
  236. package/src/route-types.ts +37 -46
  237. package/src/router/basename.ts +14 -0
  238. package/src/router/content-negotiation.ts +164 -17
  239. package/src/router/error-handling.ts +45 -18
  240. package/src/router/find-match.ts +130 -29
  241. package/src/router/handler-context.ts +83 -39
  242. package/src/router/instrument.ts +355 -0
  243. package/src/router/intercept-resolution.ts +50 -24
  244. package/src/router/lazy-includes.ts +89 -63
  245. package/src/router/loader-resolution.ts +286 -56
  246. package/src/router/logging.ts +5 -8
  247. package/src/router/manifest.ts +105 -56
  248. package/src/router/match-api.ts +178 -218
  249. package/src/router/match-context.ts +0 -22
  250. package/src/router/match-handlers.ts +211 -165
  251. package/src/router/match-middleware/background-revalidation.ts +66 -22
  252. package/src/router/match-middleware/cache-lookup.ts +214 -263
  253. package/src/router/match-middleware/cache-store.ts +105 -50
  254. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  255. package/src/router/match-middleware/segment-resolution.ts +52 -18
  256. package/src/router/match-pipelines.ts +1 -42
  257. package/src/router/match-result.ts +128 -44
  258. package/src/router/metrics.ts +5 -34
  259. package/src/router/middleware-types.ts +13 -142
  260. package/src/router/middleware.ts +301 -177
  261. package/src/router/navigation-snapshot.ts +133 -0
  262. package/src/router/params-util.ts +23 -0
  263. package/src/router/parse-pattern.ts +115 -0
  264. package/src/router/pattern-matching.ts +181 -150
  265. package/src/router/prefetch-cache-ttl.ts +51 -0
  266. package/src/router/prefetch-limits.ts +37 -0
  267. package/src/router/prerender-match.ts +203 -58
  268. package/src/router/preview-match.ts +35 -103
  269. package/src/router/request-classification.ts +291 -0
  270. package/src/router/revalidation.ts +123 -73
  271. package/src/router/route-snapshot.ts +256 -0
  272. package/src/router/router-context.ts +11 -29
  273. package/src/router/router-interfaces.ts +146 -35
  274. package/src/router/router-options.ts +202 -15
  275. package/src/router/router-registry.ts +2 -5
  276. package/src/router/segment-resolution/fresh.ts +301 -78
  277. package/src/router/segment-resolution/helpers.ts +115 -30
  278. package/src/router/segment-resolution/loader-cache.ts +156 -39
  279. package/src/router/segment-resolution/loader-mask.ts +60 -0
  280. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  281. package/src/router/segment-resolution/mask-nested.ts +83 -0
  282. package/src/router/segment-resolution/revalidation.ts +477 -385
  283. package/src/router/segment-resolution/static-store.ts +19 -5
  284. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  285. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  286. package/src/router/segment-resolution.ts +5 -1
  287. package/src/router/segment-wrappers.ts +8 -5
  288. package/src/router/state-cookie-name.ts +33 -0
  289. package/src/router/substitute-pattern-params.ts +75 -0
  290. package/src/router/telemetry-otel.ts +160 -200
  291. package/src/router/telemetry.ts +105 -20
  292. package/src/router/timeout.ts +0 -20
  293. package/src/router/tracing.ts +215 -0
  294. package/src/router/trie-matching.ts +171 -59
  295. package/src/router/types.ts +10 -63
  296. package/src/router/url-params.ts +57 -0
  297. package/src/router.ts +210 -71
  298. package/src/rsc/full-payload.ts +70 -0
  299. package/src/rsc/handler-context.ts +3 -2
  300. package/src/rsc/handler.ts +682 -508
  301. package/src/rsc/helpers.ts +168 -46
  302. package/src/rsc/index.ts +2 -5
  303. package/src/rsc/json-route-result.ts +38 -0
  304. package/src/rsc/loader-fetch.ts +127 -31
  305. package/src/rsc/manifest-init.ts +33 -42
  306. package/src/rsc/nonce.ts +10 -1
  307. package/src/rsc/origin-guard.ts +39 -25
  308. package/src/rsc/progressive-enhancement.ts +138 -15
  309. package/src/rsc/redirect-guard.ts +100 -0
  310. package/src/rsc/response-cache-serve.ts +238 -0
  311. package/src/rsc/response-error.ts +79 -12
  312. package/src/rsc/response-route-handler.ts +99 -189
  313. package/src/rsc/rsc-rendering.ts +509 -73
  314. package/src/rsc/runtime-warnings.ts +23 -10
  315. package/src/rsc/server-action.ts +287 -113
  316. package/src/rsc/shell-capture.ts +1190 -0
  317. package/src/rsc/shell-serve.ts +181 -0
  318. package/src/rsc/ssr-setup.ts +18 -2
  319. package/src/rsc/transition-gate.ts +89 -0
  320. package/src/rsc/types.ts +62 -6
  321. package/src/runtime-env.ts +18 -0
  322. package/src/search-params.ts +35 -30
  323. package/src/segment-content-promise.ts +67 -0
  324. package/src/segment-loader-promise.ts +167 -0
  325. package/src/segment-system.tsx +449 -132
  326. package/src/serialize.ts +243 -0
  327. package/src/server/context.ts +367 -61
  328. package/src/server/cookie-parse.ts +32 -0
  329. package/src/server/cookie-store.ts +152 -5
  330. package/src/server/handle-store.ts +40 -38
  331. package/src/server/loader-registry.ts +38 -46
  332. package/src/server/request-context.ts +558 -173
  333. package/src/ssr/index.tsx +491 -174
  334. package/src/ssr/inject-rsc-eager.ts +167 -0
  335. package/src/ssr/ssr-root.tsx +228 -0
  336. package/src/static-handler.ts +27 -18
  337. package/src/testing/cache-status.ts +162 -0
  338. package/src/testing/collect-handle.ts +46 -0
  339. package/src/testing/dispatch.ts +813 -0
  340. package/src/testing/dom.entry.ts +22 -0
  341. package/src/testing/e2e/fixture.ts +188 -0
  342. package/src/testing/e2e/index.ts +128 -0
  343. package/src/testing/e2e/matchers.ts +35 -0
  344. package/src/testing/e2e/page-helpers.ts +272 -0
  345. package/src/testing/e2e/parity.ts +387 -0
  346. package/src/testing/e2e/server.ts +195 -0
  347. package/src/testing/flight-matchers.ts +97 -0
  348. package/src/testing/flight-normalize.ts +11 -0
  349. package/src/testing/flight-runtime.d.ts +57 -0
  350. package/src/testing/flight-tree.ts +682 -0
  351. package/src/testing/flight.entry.ts +52 -0
  352. package/src/testing/flight.ts +257 -0
  353. package/src/testing/generated-routes.ts +199 -0
  354. package/src/testing/index.ts +105 -0
  355. package/src/testing/internal/context.ts +371 -0
  356. package/src/testing/internal/flight-client-globals.ts +30 -0
  357. package/src/testing/internal/seed-vars.ts +54 -0
  358. package/src/testing/render-handler.ts +357 -0
  359. package/src/testing/render-route.tsx +584 -0
  360. package/src/testing/run-loader.ts +385 -0
  361. package/src/testing/run-middleware.ts +205 -0
  362. package/src/testing/run-transition-when.ts +164 -0
  363. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  364. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  365. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  366. package/src/testing/vitest-stubs/version.ts +5 -0
  367. package/src/testing/vitest.ts +305 -0
  368. package/src/theme/ThemeProvider.tsx +56 -84
  369. package/src/theme/ThemeScript.tsx +7 -9
  370. package/src/theme/constants.ts +52 -13
  371. package/src/theme/index.ts +0 -7
  372. package/src/theme/theme-context.ts +1 -5
  373. package/src/theme/theme-script.ts +22 -21
  374. package/src/theme/use-theme.ts +0 -3
  375. package/src/types/boundaries.ts +0 -35
  376. package/src/types/cache-types.ts +17 -8
  377. package/src/types/error-types.ts +30 -90
  378. package/src/types/global-namespace.ts +54 -41
  379. package/src/types/handler-context.ts +234 -82
  380. package/src/types/index.ts +3 -10
  381. package/src/types/loader-types.ts +44 -15
  382. package/src/types/request-scope.ts +112 -0
  383. package/src/types/route-config.ts +20 -52
  384. package/src/types/route-entry.ts +19 -7
  385. package/src/types/segments.ts +137 -14
  386. package/src/urls/include-helper.ts +40 -75
  387. package/src/urls/include-provider.ts +71 -0
  388. package/src/urls/index.ts +2 -11
  389. package/src/urls/path-helper-types.ts +102 -23
  390. package/src/urls/path-helper.ts +62 -111
  391. package/src/urls/pattern-types.ts +84 -19
  392. package/src/urls/response-types.ts +25 -22
  393. package/src/urls/type-extraction.ts +98 -154
  394. package/src/urls/urls-function.ts +1 -19
  395. package/src/use-loader.tsx +346 -89
  396. package/src/vercel/index.ts +11 -0
  397. package/src/vercel/tracing.ts +88 -0
  398. package/src/vite/debug.ts +185 -0
  399. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  400. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  401. package/src/vite/discovery/discover-routers.ts +130 -85
  402. package/src/vite/discovery/discovery-errors.ts +255 -0
  403. package/src/vite/discovery/gate-state.ts +171 -0
  404. package/src/vite/discovery/prerender-collection.ts +214 -132
  405. package/src/vite/discovery/route-types-writer.ts +40 -84
  406. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  407. package/src/vite/discovery/state.ts +57 -6
  408. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  409. package/src/vite/index.ts +15 -0
  410. package/src/vite/inject-client-debug.ts +88 -0
  411. package/src/vite/plugin-types.ts +234 -62
  412. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  413. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  414. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  415. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  416. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  417. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  418. package/src/vite/plugins/expose-action-id.ts +49 -98
  419. package/src/vite/plugins/expose-id-utils.ts +96 -51
  420. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  421. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  422. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  423. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  424. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  425. package/src/vite/plugins/performance-tracks.ts +89 -0
  426. package/src/vite/plugins/refresh-cmd.ts +89 -27
  427. package/src/vite/plugins/use-cache-transform.ts +73 -83
  428. package/src/vite/plugins/vercel-output.ts +384 -0
  429. package/src/vite/plugins/version-injector.ts +40 -29
  430. package/src/vite/plugins/version-plugin.ts +46 -37
  431. package/src/vite/plugins/virtual-entries.ts +138 -27
  432. package/src/vite/rango.ts +353 -303
  433. package/src/vite/router-discovery.ts +1090 -166
  434. package/src/vite/utils/ast-handler-extract.ts +26 -35
  435. package/src/vite/utils/banner.ts +4 -4
  436. package/src/vite/utils/bundle-analysis.ts +10 -15
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +4 -59
  441. package/src/vite/utils/package-resolution.ts +20 -52
  442. package/src/vite/utils/prerender-utils.ts +98 -38
  443. package/src/vite/utils/shared-utils.ts +144 -44
  444. package/src/browser/action-response-classifier.ts +0 -99
  445. package/src/browser/react/use-client-cache.ts +0 -58
  446. package/src/browser/shallow.ts +0 -40
  447. package/src/handles/index.ts +0 -7
  448. package/src/network-error-thrower.tsx +0 -23
  449. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,12 @@ import type { ReactNode } from "react";
10
10
  import { invariant } from "../../errors";
11
11
  import { revalidate } from "../loader-resolution.js";
12
12
  import { evaluateRevalidation } from "../revalidation.js";
13
- import type { EntryData } from "../../server/context";
13
+ import {
14
+ getParallelEntries,
15
+ getParallelSlotEntries,
16
+ type EntryData,
17
+ type ParallelEntryData,
18
+ } from "../../server/context";
14
19
  import type {
15
20
  HandlerContext,
16
21
  InternalHandlerContext,
@@ -30,54 +35,41 @@ import {
30
35
  import { resolveLoaderData } from "./loader-cache.js";
31
36
  import {
32
37
  handleHandlerResult,
38
+ warnOnStreamedResponse,
33
39
  tryStaticHandler,
34
40
  tryStaticSlot,
35
41
  resolveLayoutComponent,
36
42
  resolveWithErrorBoundary,
43
+ buildLoaderErrorContext,
37
44
  } from "./helpers.js";
45
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
38
46
  import { getRouterContext } from "../router-context.js";
39
- import { resolveSink, safeEmit } from "../telemetry.js";
40
- import { track } from "../../server/context.js";
41
-
42
- // ---------------------------------------------------------------------------
43
- // Telemetry helpers
44
- // ---------------------------------------------------------------------------
47
+ import { observeEvent, observeHandler } from "../instrument.js";
48
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
49
+ import {
50
+ track,
51
+ RangoContext,
52
+ runInsideLoaderScope,
53
+ } from "../../server/context.js";
45
54
 
46
55
  /**
47
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
48
- * Silently no-ops when called outside RouterContext (e.g. in unit tests).
56
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
57
+ * has no cached state). User revalidate fns are bypassed in this case — see
58
+ * the call sites for the load-bearing rationale.
49
59
  */
50
- function observeStreamedHandler(
51
- promise: Promise<ReactNode>,
52
- segmentId: string,
53
- segmentType: string,
54
- pathname?: string,
55
- routeKey?: string,
56
- params?: Record<string, string>,
60
+ function traceFullRefetchedParallelSlot(
61
+ parallelId: string,
62
+ belongsToRoute: boolean,
57
63
  ): void {
58
- let routerCtx;
59
- try {
60
- routerCtx = getRouterContext();
61
- } catch {
62
- return;
63
- }
64
- if (!routerCtx?.telemetry) return;
65
- const sink = resolveSink(routerCtx.telemetry);
66
- const reqId = routerCtx.requestId;
67
- promise.catch((err: unknown) => {
68
- const errorObj = err instanceof Error ? err : new Error(String(err));
69
- safeEmit(sink, {
70
- type: "handler.error",
71
- timestamp: performance.now(),
72
- requestId: reqId,
73
- segmentId,
74
- segmentType,
75
- error: errorObj,
76
- handledByBoundary: true,
77
- pathname,
78
- routeKey,
79
- params,
80
- });
64
+ if (!isTraceActive()) return;
65
+ pushRevalidationTraceEntry({
66
+ segmentId: parallelId,
67
+ segmentType: "parallel",
68
+ belongsToRoute,
69
+ source: "parallel",
70
+ defaultShouldRevalidate: true,
71
+ finalShouldRevalidate: true,
72
+ reason: "full-refetch",
81
73
  });
82
74
  }
83
75
 
@@ -96,23 +88,14 @@ function emitRevalidationDecision(
96
88
  routeKey: string,
97
89
  shouldRevalidate: boolean,
98
90
  ): void {
99
- let routerCtx;
100
- try {
101
- routerCtx = getRouterContext();
102
- } catch {
103
- return;
104
- }
105
- if (routerCtx?.telemetry) {
106
- safeEmit(resolveSink(routerCtx.telemetry), {
107
- type: "revalidation.decision",
108
- timestamp: performance.now(),
109
- requestId: routerCtx.requestId,
110
- segmentId,
111
- pathname,
112
- routeKey,
113
- shouldRevalidate,
114
- });
115
- }
91
+ observeEvent({
92
+ type: "revalidation.decision",
93
+ timestamp: performance.now(),
94
+ segmentId,
95
+ pathname,
96
+ routeKey,
97
+ shouldRevalidate,
98
+ });
116
99
  }
117
100
 
118
101
  // ---------------------------------------------------------------------------
@@ -217,6 +200,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
217
200
  ),
218
201
  );
219
202
 
203
+ // Partial (revalidation) render path: a throwing DSL loader must still fire
204
+ // onError/loader.error. isPartial flags the reporting phase accordingly.
205
+ const errorContext = { ...buildLoaderErrorContext(ctx), isPartial: true };
206
+
220
207
  const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
221
208
  const segments: ResolvedSegment[] = loadersToRun.map(
222
209
  ({ loaderEntry, loader, segmentId, index }) => ({
@@ -228,10 +215,13 @@ export async function resolveLoadersWithRevalidation<TEnv>(
228
215
  params: ctx.params,
229
216
  loaderId: loader.$$id,
230
217
  loaderData: deps.wrapLoaderPromise(
231
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
218
+ runInsideLoaderScope(() =>
219
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
220
+ ),
232
221
  entry,
233
222
  segmentId,
234
223
  ctx.pathname,
224
+ errorContext,
235
225
  ),
236
226
  belongsToRoute,
237
227
  }),
@@ -258,26 +248,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
248
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
249
  const allLoaderSegments: ResolvedSegment[] = [];
260
250
  const allMatchedIds: string[] = [];
251
+ const seenIds = new Set<string>();
252
+
253
+ async function collectEntryLoaders(
254
+ entry: EntryData,
255
+ belongsToRoute: boolean,
256
+ shortCodeOverride?: string,
257
+ ): Promise<void> {
258
+ // Skip if all loaders from this entry have already been resolved
259
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
260
+ const loaderEntries = entry.loader ?? [];
261
+ const sc = shortCodeOverride ?? entry.shortCode;
262
+ const allAlreadySeen =
263
+ loaderEntries.length > 0 &&
264
+ loaderEntries.every((le, i) =>
265
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
266
+ );
267
+ if (!allAlreadySeen) {
268
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
269
+ entry,
270
+ context,
271
+ belongsToRoute,
272
+ clientSegmentIds,
273
+ prevParams,
274
+ request,
275
+ prevUrl,
276
+ nextUrl,
277
+ routeKey,
278
+ deps,
279
+ actionContext,
280
+ shortCodeOverride,
281
+ stale,
282
+ );
283
+ for (const seg of segments) {
284
+ if (!seenIds.has(seg.id)) {
285
+ seenIds.add(seg.id);
286
+ allLoaderSegments.push(seg);
287
+ }
288
+ }
289
+ allMatchedIds.push(...matchedIds);
290
+ }
291
+
292
+ const seenParallelEntryIds = new Set<string>();
293
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
294
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
295
+ seenParallelEntryIds.add(parallelEntry.id);
296
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
297
+ }
298
+
299
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
300
+ for (const layoutEntry of entry.layout) {
301
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
302
+ // Inherit route loaders for orphan layouts with parallels.
303
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
304
+ // route entry, as that would re-iterate route.layout and loop.
305
+ if (
306
+ entry.type === "route" &&
307
+ entry.loader &&
308
+ entry.loader.length > 0 &&
309
+ Object.keys(layoutEntry.parallel).length > 0
310
+ ) {
311
+ const inherited = await resolveLoadersWithRevalidation(
312
+ entry,
313
+ context,
314
+ childBelongsToRoute,
315
+ clientSegmentIds,
316
+ prevParams,
317
+ request,
318
+ prevUrl,
319
+ nextUrl,
320
+ routeKey,
321
+ deps,
322
+ actionContext,
323
+ layoutEntry.shortCode,
324
+ stale,
325
+ );
326
+ for (const seg of inherited.segments) {
327
+ if (!seenIds.has(seg.id)) {
328
+ seenIds.add(seg.id);
329
+ seg._inherited = true;
330
+ allLoaderSegments.push(seg);
331
+ }
332
+ }
333
+ allMatchedIds.push(...inherited.matchedIds);
334
+ }
335
+ }
336
+ }
261
337
 
262
338
  for (const entry of entries) {
263
- const belongsToRoute = entry.type === "route";
264
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
265
- entry,
266
- context,
267
- belongsToRoute,
268
- clientSegmentIds,
269
- prevParams,
270
- request,
271
- prevUrl,
272
- nextUrl,
273
- routeKey,
274
- deps,
275
- actionContext,
276
- undefined, // shortCodeOverride
277
- stale,
278
- );
279
- allLoaderSegments.push(...segments);
280
- allMatchedIds.push(...matchedIds);
339
+ await collectEntryLoaders(entry, entry.type === "route");
281
340
  }
282
341
 
283
342
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +360,20 @@ export function buildEntryRevalidateMap(
301
360
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
361
 
303
362
  if (entry.type !== "parallel") {
304
- for (const parallelEntry of entry.parallel) {
305
- if (parallelEntry.type === "parallel") {
306
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
307
- for (const slot of slots) {
308
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
309
- map.set(parallelId, {
310
- entry: parallelEntry,
311
- revalidate: parallelEntry.revalidate,
312
- });
313
- }
314
- }
363
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
364
+ entry.parallel,
365
+ )) {
366
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
367
+ const parallelId = `${parallelParentShortCode}.${slot}`;
368
+ map.set(parallelId, {
369
+ entry: parallelEntry,
370
+ revalidate: parallelEntry.revalidate,
371
+ });
315
372
  }
316
373
  }
317
374
 
318
375
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
376
+ processEntry(layoutEntry, entry.shortCode);
320
377
  }
321
378
  }
322
379
 
@@ -327,6 +384,97 @@ export function buildEntryRevalidateMap(
327
384
  return map;
328
385
  }
329
386
 
387
+ /**
388
+ * Resolve the component for a single parallel slot on the revalidation path.
389
+ * Pure component resolution shared verbatim by
390
+ * resolveParallelSegmentsWithRevalidation and the orphan-inlined loop in
391
+ * resolveOrphanLayoutWithRevalidation: try the static slot cache, else run the
392
+ * slot handler (pinning _currentSegmentId to the slot id so handle pushes land
393
+ * in the slot's own bucket, and wrapping a streamed handler). Returns the
394
+ * resolved component and whether the handler actually ran. Does NOT touch the
395
+ * revalidate-default policy (the caller decides shouldResolve, including the
396
+ * orphan-vs-main defaultOverride divergence) or loader-resolution ordering.
397
+ */
398
+ async function resolveParallelSlotComponent<TEnv>(args: {
399
+ shouldResolve: boolean;
400
+ parallelEntry: ParallelEntryData;
401
+ slot: string;
402
+ parallelId: string;
403
+ handler:
404
+ | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
405
+ | ReactNode
406
+ | undefined;
407
+ context: HandlerContext<any, TEnv>;
408
+ deps: SegmentResolutionDeps<TEnv>;
409
+ routeKey: string;
410
+ params: Record<string, string>;
411
+ }): Promise<{ component: ReactNode | undefined; handlerRan: boolean }> {
412
+ const {
413
+ shouldResolve,
414
+ parallelEntry,
415
+ slot,
416
+ parallelId,
417
+ handler,
418
+ context,
419
+ deps,
420
+ routeKey,
421
+ params,
422
+ } = args;
423
+
424
+ let component: ReactNode | undefined;
425
+ let handlerRan = false;
426
+ if (shouldResolve) {
427
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
428
+ // tryStaticSlot returning a value means the static cache supplied the
429
+ // component — handler did NOT run. handlerRan stays false.
430
+ }
431
+ if (component === undefined) {
432
+ const hasLoadingFallback =
433
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
434
+ if (!shouldResolve) {
435
+ component = null;
436
+ } else if (handler === undefined) {
437
+ // Handler evicted (production static slot) but static lookup missed.
438
+ // Nothing to render — use null so the client keeps its cached version.
439
+ component = null;
440
+ } else {
441
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns its
442
+ // own. On slot-only revalidations the partial merge updates only the
443
+ // slot's bucket; the parent's bucket stays intact.
444
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
445
+ parallelId;
446
+ handlerRan = true;
447
+ if (hasLoadingFallback) {
448
+ const result =
449
+ typeof handler === "function" ? handler(context) : handler;
450
+ if (result instanceof Promise) {
451
+ warnOnStreamedResponse(result, parallelId);
452
+ const tracked = deps.trackHandler(result, {
453
+ segmentId: parallelId,
454
+ segmentType: "parallel",
455
+ });
456
+ observeStreamedHandler(
457
+ tracked,
458
+ parallelId,
459
+ "parallel",
460
+ context.pathname,
461
+ routeKey,
462
+ params,
463
+ );
464
+ component = tracked as ReactNode;
465
+ } else {
466
+ component = result as ReactNode;
467
+ }
468
+ } else {
469
+ component =
470
+ typeof handler === "function" ? await handler(context) : handler;
471
+ }
472
+ }
473
+ }
474
+
475
+ return { component, handlerRan };
476
+ }
477
+
330
478
  /**
331
479
  * Resolve parallel segments with revalidation.
332
480
  */
@@ -344,11 +492,35 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
344
492
  deps: SegmentResolutionDeps<TEnv>,
345
493
  actionContext?: ActionContext,
346
494
  stale?: boolean,
495
+ options?: {
496
+ /**
497
+ * Seed for an unknown parent-chain slot (slot not in clientSegmentIds) when
498
+ * there are no deciding revalidate fns. "type-derived" (default, main path):
499
+ * `belongsToRoute || isNewParent`. "force-render" (orphan path): always
500
+ * `true` — orphan parallels always belong to the route and must render
501
+ * unless the user opts out via revalidate(); the #482 blank-parent-chain-
502
+ * slot guard.
503
+ */
504
+ parentChainDefault?: "type-derived" | "force-render";
505
+ /**
506
+ * When a slot's loaders are resolved relative to the slot segment push.
507
+ * "after" (default, main path) pushes the slot segment first; "before"
508
+ * (orphan path) resolves loaders first. This only changes the
509
+ * segments/matchedIds emission ORDER (the client reconciler is insensitive
510
+ * to it: loader sub-ids are filtered out and slots are re-grouped by parent).
511
+ */
512
+ loaderOrder?: "after" | "before";
513
+ },
347
514
  ): Promise<SegmentRevalidationResult> {
348
515
  const segments: ResolvedSegment[] = [];
349
516
  const matchedIds: string[] = [];
517
+ const parentChainDefault = options?.parentChainDefault ?? "type-derived";
518
+ const loaderOrder = options?.loaderOrder ?? "after";
350
519
 
351
- for (const parallelEntry of entry.parallel) {
520
+ const resolvedParallelEntries = new Set<string>();
521
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
522
+ entry.parallel,
523
+ )) {
352
524
  invariant(
353
525
  parallelEntry.type === "parallel",
354
526
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +531,78 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
531
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
532
  | ReactNode
361
533
  >;
534
+ // In production, static handler bodies are evicted and the slot value
535
+ // may be undefined. The static store holds the pre-rendered component.
536
+ // We defer the handler check until after tryStaticSlot.
537
+ const handler = slots[slot];
362
538
 
363
- for (const [slot, handler] of Object.entries(slots)) {
364
- const parallelId = `${entry.shortCode}.${slot}`;
539
+ const parallelId = `${entry.shortCode}.${slot}`;
365
540
 
366
- const isFullRefetch = clientSegmentIds.size === 0;
367
- // When the parent layout is new (not in client's segment set),
368
- // all its parallel children must be resolved and tracked.
369
- // Without this, navigating to a new layout with parallels
370
- // (e.g., BlogLayout with @sidebar) from a different route
371
- // would silently drop those parallel segments.
372
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
373
- if (
374
- isFullRefetch ||
375
- clientSegmentIds.has(parallelId) ||
376
- belongsToRoute ||
377
- isNewParent
378
- ) {
379
- matchedIds.push(parallelId);
380
- }
541
+ const isFullRefetch = clientSegmentIds.size === 0;
542
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
381
543
 
382
- const shouldResolve = await (async () => {
383
- if (isFullRefetch) {
384
- if (isTraceActive()) {
385
- pushRevalidationTraceEntry({
386
- segmentId: parallelId,
387
- segmentType: "parallel",
388
- belongsToRoute,
389
- source: "parallel",
390
- defaultShouldRevalidate: true,
391
- finalShouldRevalidate: true,
392
- reason: "full-refetch",
393
- });
394
- }
395
- return true;
396
- }
397
- if (!clientSegmentIds.has(parallelId)) {
398
- const result = belongsToRoute || isNewParent;
399
- if (isTraceActive()) {
400
- pushRevalidationTraceEntry({
401
- segmentId: parallelId,
402
- segmentType: "parallel",
403
- belongsToRoute,
404
- source: "parallel",
405
- defaultShouldRevalidate: result,
406
- finalShouldRevalidate: result,
407
- reason: result ? "new-segment" : "skip-parent-chain",
408
- });
409
- }
410
- return result;
411
- }
412
-
413
- const dummySegment: ResolvedSegment = {
414
- id: parallelId,
415
- namespace: parallelEntry.id,
416
- type: "parallel",
417
- index: 0,
418
- component: null as any,
419
- params,
420
- slot,
421
- belongsToRoute,
422
- parallelName: `${parallelEntry.id}.${slot}`,
423
- ...(parallelEntry.mountPath
424
- ? { mountPath: parallelEntry.mountPath }
425
- : {}),
426
- };
427
-
428
- return await evaluateRevalidation({
429
- segment: dummySegment,
430
- prevParams,
431
- getPrevSegment: null,
432
- request,
433
- prevUrl,
434
- nextUrl,
435
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
436
- name: `revalidate${i}`,
437
- fn,
438
- })),
439
- routeKey,
440
- context,
441
- actionContext,
442
- stale,
443
- traceSource: "parallel",
444
- });
445
- })();
446
- emitRevalidationDecision(
447
- parallelId,
448
- context.pathname,
544
+ // A slot's loaders (never cached) are deduped per parallel entry and
545
+ // emitted either before or after the slot segment per loaderOrder.
546
+ const resolveSlotLoaders = async () => {
547
+ if (resolvedParallelEntries.has(parallelEntry.id)) return;
548
+ const loaderResult = await resolveLoadersWithRevalidation(
549
+ parallelEntry,
550
+ context,
551
+ belongsToRoute,
552
+ clientSegmentIds,
553
+ prevParams,
554
+ request,
555
+ prevUrl,
556
+ nextUrl,
449
557
  routeKey,
450
- shouldResolve,
558
+ deps,
559
+ actionContext,
560
+ entry.shortCode,
561
+ stale,
451
562
  );
563
+ segments.push(...loaderResult.segments);
564
+ matchedIds.push(...loaderResult.matchedIds);
565
+ resolvedParallelEntries.add(parallelEntry.id);
566
+ };
452
567
 
453
- let component: ReactNode | undefined;
454
- if (shouldResolve) {
455
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
456
- }
457
- if (component === undefined) {
458
- const hasLoadingFallback =
459
- parallelEntry.loading !== undefined &&
460
- parallelEntry.loading !== false;
461
- if (!shouldResolve) {
462
- component = null;
463
- } else if (hasLoadingFallback) {
464
- const result =
465
- typeof handler === "function" ? handler(context) : handler;
466
- if (result instanceof Promise) {
467
- const tracked = deps.trackHandler(result, {
468
- segmentId: parallelId,
469
- segmentType: "parallel",
470
- });
471
- observeStreamedHandler(
472
- tracked,
473
- parallelId,
474
- "parallel",
475
- context.pathname,
476
- routeKey,
477
- params,
478
- );
479
- component = tracked as ReactNode;
480
- } else {
481
- component = result as ReactNode;
482
- }
483
- } else {
484
- component =
485
- typeof handler === "function" ? await handler(context) : handler;
486
- }
568
+ if (loaderOrder === "before") {
569
+ await resolveSlotLoaders();
570
+ }
571
+ // Always announce the slot in matchedIds — it's unconditionally appended
572
+ // to `segments` below, and a segment present in segments but missing from
573
+ // matched lets the client prune it (then it's missing from clientSegmentIds
574
+ // on the next request, perpetuating the staleness).
575
+ matchedIds.push(parallelId);
576
+
577
+ let shouldResolve: boolean;
578
+ if (isFullRefetch) {
579
+ // Client has nothing cached — slot MUST render. User revalidate fns are
580
+ // bypassed here because returning false would leave the segment blank
581
+ // with no client-side fallback.
582
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
583
+ shouldResolve = true;
584
+ } else {
585
+ // For non-empty client sets, consult user revalidate fns. When the slot
586
+ // is unknown to the client, override the type-derived default so the
587
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
588
+ let defaultOverride: { value: boolean; reason: string } | undefined;
589
+ if (!clientSegmentIds.has(parallelId)) {
590
+ const value =
591
+ parentChainDefault === "force-render"
592
+ ? true
593
+ : belongsToRoute || isNewParent;
594
+ defaultOverride = {
595
+ value,
596
+ reason: value ? "new-segment" : "skip-parent-chain",
597
+ };
487
598
  }
488
599
 
489
- segments.push({
600
+ const dummySegment: ResolvedSegment = {
490
601
  id: parallelId,
491
602
  namespace: parallelEntry.id,
492
603
  type: "parallel",
493
604
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
605
+ component: null as any,
497
606
  params,
498
607
  slot,
499
608
  belongsToRoute,
@@ -501,27 +610,70 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
610
  ...(parallelEntry.mountPath
502
611
  ? { mountPath: parallelEntry.mountPath }
503
612
  : {}),
504
- });
505
- }
613
+ };
506
614
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
615
+ shouldResolve = await evaluateRevalidation({
616
+ segment: dummySegment,
513
617
  prevParams,
618
+ getPrevSegment: null,
514
619
  request,
515
620
  prevUrl,
516
621
  nextUrl,
622
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
623
+ name: `revalidate${i}`,
624
+ fn,
625
+ })),
517
626
  routeKey,
518
- deps,
627
+ context,
519
628
  actionContext,
520
- entry.shortCode,
521
629
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
630
+ traceSource: "parallel",
631
+ defaultOverride,
632
+ });
633
+ }
634
+ emitRevalidationDecision(
635
+ parallelId,
636
+ context.pathname,
637
+ routeKey,
638
+ shouldResolve,
639
+ );
640
+
641
+ const { component, handlerRan } = await resolveParallelSlotComponent({
642
+ shouldResolve,
643
+ parallelEntry,
644
+ slot,
645
+ parallelId,
646
+ handler,
647
+ context,
648
+ deps,
649
+ routeKey,
650
+ params,
651
+ });
652
+
653
+ segments.push({
654
+ id: parallelId,
655
+ namespace: parallelEntry.id,
656
+ type: "parallel",
657
+ index: 0,
658
+ component,
659
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
660
+ transition: applyViewTransitionDefault(
661
+ parallelEntry.transition,
662
+ deps.viewTransitionDefault,
663
+ parallelId,
664
+ ),
665
+ params,
666
+ slot,
667
+ _handlerRan: handlerRan,
668
+ belongsToRoute,
669
+ parallelName: `${parallelEntry.id}.${slot}`,
670
+ ...(parallelEntry.mountPath
671
+ ? { mountPath: parallelEntry.mountPath }
672
+ : {}),
673
+ });
674
+
675
+ if (loaderOrder === "after") {
676
+ await resolveSlotLoaders();
525
677
  }
526
678
  }
527
679
 
@@ -548,6 +700,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
548
700
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
549
701
  const matchedId = entry.shortCode;
550
702
 
703
+ let handlerRan = false;
551
704
  const component = await revalidate(
552
705
  async () => {
553
706
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -608,6 +761,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
761
  context,
609
762
  actionContext,
610
763
  stale,
764
+ traceSource:
765
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
766
  });
612
767
  emitRevalidationDecision(
613
768
  entry.shortCode,
@@ -622,6 +777,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
622
777
  return shouldRevalidate;
623
778
  },
624
779
  async () => {
780
+ handlerRan = true;
625
781
  const doneHandler = track(`handler:${entry.id}`, 2);
626
782
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
627
783
  entry.shortCode;
@@ -636,14 +792,26 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
636
792
  return staticComponent;
637
793
  }
638
794
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
795
+ // For Passthrough routes at runtime, use the live handler instead of
796
+ // the build handler. At build time (context.build === true), always
797
+ // use the build handler from routeEntry.handler.
798
+ const handler =
799
+ !context.build && routeEntry.liveHandler
800
+ ? routeEntry.liveHandler
801
+ : routeEntry.handler;
639
802
  if (!routeEntry.loading) {
640
- const result = handleHandlerResult(await routeEntry.handler(context));
803
+ const result = handleHandlerResult(
804
+ await observeHandler(entry.id, handler, context),
805
+ );
641
806
  doneHandler();
642
807
  return result;
643
808
  }
644
809
  if (!actionContext) {
645
- const result = handleHandlerResult(routeEntry.handler(context));
810
+ const result = handleHandlerResult(
811
+ observeHandler(entry.id, handler, context),
812
+ );
646
813
  if (result instanceof Promise) {
814
+ warnOnStreamedResponse(result, routeEntry.id);
647
815
  result.finally(doneHandler).catch(() => {});
648
816
  const tracked = deps.trackHandler(result, {
649
817
  segmentId: entry.shortCode,
@@ -666,7 +834,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
666
834
  entryId: entry.id,
667
835
  });
668
836
  const actionResult = handleHandlerResult(
669
- await routeEntry.handler(context),
837
+ await observeHandler(entry.id, handler, context),
670
838
  );
671
839
  doneHandler();
672
840
  return {
@@ -676,10 +844,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
844
  () => null,
677
845
  );
678
846
 
847
+ // Normalize void handlers (undefined) to null so the reconciler's
848
+ // component === null checks work consistently for both void and explicit null.
679
849
  const resolvedComponent =
680
850
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
851
+ ? ((component as { content: ReactNode }).content ?? null)
852
+ : (component ?? null);
683
853
 
684
854
  const segment: ResolvedSegment = {
685
855
  id: entry.shortCode,
@@ -689,13 +859,18 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
689
859
  index: 0,
690
860
  component: resolvedComponent,
691
861
  loading: entry.loading === false ? null : entry.loading,
692
- transition: entry.transition,
862
+ transition: applyViewTransitionDefault(
863
+ entry.transition,
864
+ deps.viewTransitionDefault,
865
+ entry.shortCode,
866
+ ),
693
867
  params,
694
868
  belongsToRoute,
695
869
  ...(entry.type === "layout" || entry.type === "cache"
696
870
  ? { layoutName: entry.id }
697
871
  : {}),
698
872
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
873
+ _handlerRan: handlerRan,
699
874
  };
700
875
 
701
876
  return { segment, matchedId };
@@ -714,7 +889,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
714
889
  request: Request,
715
890
  prevUrl: URL,
716
891
  nextUrl: URL,
717
- loaderPromises: Map<string, Promise<any>>,
718
892
  deps: SegmentResolutionDeps<TEnv>,
719
893
  actionContext?: ActionContext,
720
894
  stale?: boolean,
@@ -776,11 +950,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
776
950
  prevUrl,
777
951
  nextUrl,
778
952
  routeKey,
779
- loaderPromises,
780
953
  true,
781
954
  deps,
782
955
  actionContext,
783
956
  stale,
957
+ entry,
784
958
  );
785
959
  segments.push(...orphanResult.segments);
786
960
  matchedIds.push(...orphanResult.matchedIds);
@@ -860,7 +1034,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
860
1034
  prevUrl,
861
1035
  nextUrl,
862
1036
  routeKey,
863
- loaderPromises,
864
1037
  false,
865
1038
  deps,
866
1039
  actionContext,
@@ -887,11 +1060,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
887
1060
  prevUrl: URL,
888
1061
  nextUrl: URL,
889
1062
  routeKey: string,
890
- loaderPromises: Map<string, Promise<any>>,
891
1063
  belongsToRoute: boolean,
892
1064
  deps: SegmentResolutionDeps<TEnv>,
893
1065
  actionContext?: ActionContext,
894
1066
  stale?: boolean,
1067
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1068
+ parentRouteEntry?: EntryData,
895
1069
  ): Promise<SegmentRevalidationResult> {
896
1070
  invariant(
897
1071
  orphan.type === "layout" || orphan.type === "cache",
@@ -919,6 +1093,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
919
1093
  segments.push(...loaderResult.segments);
920
1094
  matchedIds.push(...loaderResult.matchedIds);
921
1095
 
1096
+ // Inherit parent route's loaders so parallel slots inside this layout
1097
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1098
+ if (
1099
+ parentRouteEntry &&
1100
+ parentRouteEntry.loader &&
1101
+ parentRouteEntry.loader.length > 0 &&
1102
+ Object.keys(orphan.parallel).length > 0
1103
+ ) {
1104
+ const inheritedResult = await resolveLoadersWithRevalidation(
1105
+ parentRouteEntry,
1106
+ context,
1107
+ belongsToRoute,
1108
+ clientSegmentIds,
1109
+ prevParams,
1110
+ request,
1111
+ prevUrl,
1112
+ nextUrl,
1113
+ routeKey,
1114
+ deps,
1115
+ actionContext,
1116
+ orphan.shortCode,
1117
+ stale,
1118
+ );
1119
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1120
+ for (const s of inheritedResult.segments) {
1121
+ s._inherited = true;
1122
+ }
1123
+ segments.push(...inheritedResult.segments);
1124
+ matchedIds.push(...inheritedResult.matchedIds);
1125
+ }
1126
+
922
1127
  // Handler-first: resolve orphan layout handler before its parallels
923
1128
  // so ctx.set() values are visible to parallel children.
924
1129
  matchedIds.push(orphan.shortCode);
@@ -991,157 +1196,41 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
991
1196
  belongsToRoute,
992
1197
  layoutName: orphan.id,
993
1198
  loading: orphan.loading === false ? null : orphan.loading,
994
- transition: orphan.transition,
1199
+ transition: applyViewTransitionDefault(
1200
+ orphan.transition,
1201
+ deps.viewTransitionDefault,
1202
+ orphan.shortCode,
1203
+ ),
995
1204
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1205
  });
997
1206
 
998
- for (const parallelEntry of orphan.parallel) {
999
- invariant(
1000
- parallelEntry.type === "parallel",
1001
- `Expected parallel entry, got: ${parallelEntry.type}`,
1002
- );
1003
-
1004
- const loaderResult = await resolveLoadersWithRevalidation(
1005
- parallelEntry,
1006
- context,
1007
- belongsToRoute,
1008
- clientSegmentIds,
1009
- prevParams,
1010
- request,
1011
- prevUrl,
1012
- nextUrl,
1013
- routeKey,
1014
- deps,
1015
- actionContext,
1016
- undefined,
1017
- stale,
1018
- );
1019
- segments.push(...loaderResult.segments);
1020
- matchedIds.push(...loaderResult.matchedIds);
1021
-
1022
- const slots = parallelEntry.handler as Record<
1023
- `@${string}`,
1024
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
- | ReactNode
1026
- >;
1027
-
1028
- for (const [slot, handler] of Object.entries(slots)) {
1029
- // Use orphan.shortCode (the parent layout) to match the SSR path
1030
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1031
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1032
- const parallelId = `${orphan.shortCode}.${slot}`;
1033
- matchedIds.push(parallelId);
1034
-
1035
- const shouldResolve = await (async () => {
1036
- if (!clientSegmentIds.has(parallelId)) {
1037
- if (isTraceActive()) {
1038
- pushRevalidationTraceEntry({
1039
- segmentId: parallelId,
1040
- segmentType: "parallel",
1041
- belongsToRoute,
1042
- source: "parallel",
1043
- defaultShouldRevalidate: true,
1044
- finalShouldRevalidate: true,
1045
- reason: "new-segment",
1046
- });
1047
- }
1048
- return true;
1049
- }
1050
-
1051
- const dummySegment: ResolvedSegment = {
1052
- id: parallelId,
1053
- namespace: parallelEntry.id,
1054
- type: "parallel",
1055
- index: 0,
1056
- component: null as any,
1057
- params,
1058
- slot,
1059
- belongsToRoute,
1060
- parallelName: `${parallelEntry.id}.${slot}`,
1061
- ...(parallelEntry.mountPath
1062
- ? { mountPath: parallelEntry.mountPath }
1063
- : {}),
1064
- };
1065
-
1066
- return await evaluateRevalidation({
1067
- segment: dummySegment,
1068
- prevParams,
1069
- getPrevSegment: null,
1070
- request,
1071
- prevUrl,
1072
- nextUrl,
1073
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1074
- name: `revalidate${i}`,
1075
- fn,
1076
- })),
1077
- routeKey,
1078
- context,
1079
- actionContext,
1080
- stale,
1081
- traceSource: "parallel",
1082
- });
1083
- })();
1084
- emitRevalidationDecision(
1085
- parallelId,
1086
- context.pathname,
1087
- routeKey,
1088
- shouldResolve,
1089
- );
1090
-
1091
- let component: ReactNode | undefined;
1092
- if (shouldResolve) {
1093
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1094
- }
1095
- if (component === undefined) {
1096
- const hasLoadingFallback =
1097
- parallelEntry.loading !== undefined &&
1098
- parallelEntry.loading !== false;
1099
- if (!shouldResolve) {
1100
- component = null;
1101
- } else if (hasLoadingFallback) {
1102
- const result =
1103
- typeof handler === "function" ? handler(context) : handler;
1104
- if (result instanceof Promise) {
1105
- const tracked = deps.trackHandler(result, {
1106
- segmentId: parallelId,
1107
- segmentType: "parallel",
1108
- });
1109
- observeStreamedHandler(
1110
- tracked,
1111
- parallelId,
1112
- "parallel",
1113
- context.pathname,
1114
- routeKey,
1115
- params,
1116
- );
1117
- component = tracked as ReactNode;
1118
- } else {
1119
- component = result as ReactNode;
1120
- }
1121
- } else {
1122
- component =
1123
- typeof handler === "function" ? await handler(context) : handler;
1124
- }
1125
- }
1126
-
1127
- segments.push({
1128
- id: parallelId,
1129
- namespace: parallelEntry.id,
1130
- type: "parallel",
1131
- index: 0,
1132
- component,
1133
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1134
- transition: parallelEntry.transition,
1135
- params,
1136
- slot,
1137
- belongsToRoute,
1138
- parallelName: `${parallelEntry.id}.${slot}`,
1139
- ...(parallelEntry.mountPath
1140
- ? { mountPath: parallelEntry.mountPath }
1141
- : {}),
1142
- });
1143
- }
1144
- }
1207
+ // Resolve the orphan layout's parallel slots through the shared main-path
1208
+ // helper. The orphan policy is carried by explicit args, byte-for-byte:
1209
+ // - parentChainDefault "force-render": an unknown parent-chain slot seeds
1210
+ // `true` (orphan parallels always belong to the route — the #482 guard),
1211
+ // where the main path would seed `belongsToRoute || isNewParent`.
1212
+ // - loaderOrder "before": a slot's loaders are emitted before the slot
1213
+ // segment, matching the prior inlined order.
1214
+ // `entry.shortCode` inside the helper is `orphan.shortCode` (orphan is passed
1215
+ // as `entry`), so the parallel ids + loader shortCodeOverride are unchanged.
1216
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
1217
+ orphan,
1218
+ params,
1219
+ context,
1220
+ belongsToRoute,
1221
+ clientSegmentIds,
1222
+ prevParams,
1223
+ request,
1224
+ prevUrl,
1225
+ nextUrl,
1226
+ routeKey,
1227
+ deps,
1228
+ actionContext,
1229
+ stale,
1230
+ { parentChainDefault: "force-render", loaderOrder: "before" },
1231
+ );
1232
+ segments.push(...parallelResult.segments);
1233
+ matchedIds.push(...parallelResult.matchedIds);
1145
1234
 
1146
1235
  return { segments, matchedIds };
1147
1236
  }
@@ -1159,12 +1248,12 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1159
1248
  request: Request,
1160
1249
  prevUrl: URL,
1161
1250
  nextUrl: URL,
1162
- loaderPromises: Map<string, Promise<any>>,
1163
1251
  actionContext: ActionContext | undefined,
1164
1252
  interceptResult: { intercept: any; entry: EntryData } | null,
1165
1253
  localRouteName: string,
1166
1254
  pathname: string,
1167
1255
  deps: SegmentResolutionDeps<TEnv>,
1256
+ stale?: boolean,
1168
1257
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1258
  const allSegments: ResolvedSegment[] = [];
1170
1259
  const matchedIds: string[] = [];
@@ -1191,6 +1280,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1280
  }
1192
1281
 
1193
1282
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1283
+ if (entry.type === "cache") {
1284
+ const store = RangoContext.getStore();
1285
+ if (store) store.insideCacheScope = true;
1286
+ }
1194
1287
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1288
  const resolved = await resolveWithErrorBoundary(
1196
1289
  nonParallelEntry,
@@ -1206,10 +1299,9 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1206
1299
  request,
1207
1300
  prevUrl,
1208
1301
  nextUrl,
1209
- loaderPromises,
1210
1302
  deps,
1211
1303
  actionContext,
1212
- false,
1304
+ stale,
1213
1305
  ),
1214
1306
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1307
  deps,