@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.141

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +293 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2508 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +24 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-snapshot.ts +368 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +222 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1113 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +173 -35
  205. package/src/index.ts +241 -73
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +527 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +897 -0
  313. package/src/rsc/shell-serve.ts +124 -0
  314. package/src/rsc/ssr-setup.ts +144 -0
  315. package/src/rsc/transition-gate.ts +89 -0
  316. package/src/rsc/types.ts +95 -12
  317. package/src/runtime-env.ts +18 -0
  318. package/src/search-params.ts +99 -82
  319. package/src/segment-content-promise.ts +67 -0
  320. package/src/segment-loader-promise.ts +149 -0
  321. package/src/segment-system.tsx +349 -134
  322. package/src/serialize.ts +243 -0
  323. package/src/server/context.ts +459 -85
  324. package/src/server/cookie-parse.ts +32 -0
  325. package/src/server/cookie-store.ts +310 -0
  326. package/src/server/fetchable-loader-store.ts +11 -6
  327. package/src/server/handle-store.ts +123 -42
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +848 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +443 -135
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +76 -98
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +44 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +280 -0
  389. package/src/urls/pattern-types.ts +160 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  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 +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -5,8 +5,9 @@
5
5
  */
6
6
 
7
7
  import type { ReactNode } from "react";
8
- import { track } from "../server/context";
9
8
  import type { EntryData } from "../server/context";
9
+ import { observePhase, PHASES } from "./instrument.js";
10
+ import { contextGet } from "../context-var.js";
10
11
  import type {
11
12
  ResolvedSegment,
12
13
  HandlerContext,
@@ -18,11 +19,17 @@ import type {
18
19
  ErrorBoundaryFallbackProps,
19
20
  ErrorInfo,
20
21
  } from "../types";
21
- import type { LoaderRevalidationResult, ActionContext } from "./types";
22
- import { isHandle, type Handle } from "../handle.js";
23
- import type { HandleStore } from "../server/handle-store.js";
22
+ import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
+ import { buildHandleSnapshot } from "../server/handle-store.js";
24
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
25
- import { getRequestContext } from "../server/request-context.js";
26
+ import { _getRequestContext } from "../server/request-context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
32
+ } from "../server/context.js";
26
33
  import { debugLog } from "./logging.js";
27
34
 
28
35
  /**
@@ -36,7 +43,7 @@ export type LoaderErrorCallback = (
36
43
  segmentId: string;
37
44
  loaderName: string;
38
45
  handledByBoundary: boolean;
39
- }
46
+ },
40
47
  ) => void;
41
48
 
42
49
  /**
@@ -55,16 +62,18 @@ export function wrapLoaderWithErrorHandling<T>(
55
62
  segmentId: string,
56
63
  pathname: string,
57
64
  findNearestErrorBoundary: (
58
- entry: EntryData | null
65
+ entry: EntryData | null,
59
66
  ) => ReactNode | ErrorBoundaryHandler | null,
60
67
  createErrorInfo: (
61
68
  error: unknown,
62
69
  segmentId: string,
63
- segmentType: ErrorInfo["segmentType"]
70
+ segmentType: ErrorInfo["segmentType"],
64
71
  ) => ErrorInfo,
65
- onError?: LoaderErrorCallback
72
+ onError?: LoaderErrorCallback,
66
73
  ): Promise<LoaderDataResult<T>> {
67
- // Extract loader name from segmentId (format: "M1L0D0.loaderName")
74
+ // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
75
+ // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
76
+ // not a clean display name.
68
77
  const loaderName = segmentId.split(".").pop() || "unknown";
69
78
 
70
79
  return Promise.resolve(promise)
@@ -73,7 +82,7 @@ export function wrapLoaderWithErrorHandling<T>(
73
82
  __loaderResult: true,
74
83
  ok: true,
75
84
  data,
76
- })
85
+ }),
77
86
  )
78
87
  .catch((error): LoaderDataResult<T> => {
79
88
  // Find nearest error boundary
@@ -100,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
100
109
  };
101
110
  }
102
111
 
103
- // Render fallback on server
112
+ // Render fallback on server. The user ErrorBoundaryHandler may throw
113
+ // synchronously; if it does we must NOT let that rejection escape — the
114
+ // wrapped LoaderDataResult promise is contracted to never reject (see
115
+ // segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
116
+ // rejection here would collapse the whole entry and discard healthy
117
+ // sibling loader data. On a fallback-render throw, fall back to the
118
+ // no-boundary result (fallback: null) so the client throws the ORIGINAL
119
+ // error, and the wrapped promise still resolves to a LoaderDataResult.
104
120
  let renderedFallback: ReactNode;
105
- if (typeof fallback === "function") {
106
- // ErrorBoundaryHandler - call with error info
107
- const props: ErrorBoundaryFallbackProps = {
121
+ try {
122
+ if (typeof fallback === "function") {
123
+ // ErrorBoundaryHandler - call with error info
124
+ const props: ErrorBoundaryFallbackProps = {
125
+ error: errorInfo,
126
+ };
127
+ renderedFallback = fallback(props);
128
+ } else {
129
+ renderedFallback = fallback;
130
+ }
131
+ } catch (fallbackError) {
132
+ debugLog("loader", "error boundary fallback render threw", {
133
+ segmentId,
134
+ message: errorInfo.message,
135
+ fallbackError:
136
+ fallbackError instanceof Error
137
+ ? fallbackError.message
138
+ : String(fallbackError),
139
+ });
140
+ return {
141
+ __loaderResult: true,
142
+ ok: false,
108
143
  error: errorInfo,
144
+ fallback: null,
109
145
  };
110
- renderedFallback = fallback(props);
111
- } else {
112
- renderedFallback = fallback;
113
146
  }
114
147
 
115
148
  debugLog("loader", "loader error wrapped with boundary fallback", {
@@ -127,61 +160,103 @@ export function wrapLoaderWithErrorHandling<T>(
127
160
  }
128
161
 
129
162
  /**
130
- * Set up the use() method on handler context to access loaders and handles.
131
- *
132
- * For loaders: Lazily runs loaders, memoizes results per request.
133
- * For handles: Returns a push function bound to the current segment.
163
+ * Detect cycles in the loader dependency graph using DFS from a given node.
164
+ * Returns the cycle path (array of loader IDs forming the cycle) if one exists,
165
+ * or null if no cycle is found.
134
166
  */
135
- export function setupLoaderAccess<TEnv>(
136
- ctx: HandlerContext<any, TEnv>,
137
- loaderPromises: Map<string, Promise<any>>
138
- ): void {
139
- // Get HandleStore from request context
140
- const getHandleStore = (): HandleStore | undefined => {
141
- return getRequestContext()?._handleStore;
142
- };
167
+ function detectLoaderCycle(
168
+ from: string,
169
+ to: string,
170
+ dependsOn: Map<string, Set<string>>,
171
+ ): string[] | null {
172
+ // If `to` can reach `from` via the dependency graph, adding the edge
173
+ // from -> to creates a cycle. We search from `to` looking for `from`.
174
+ const visited = new Set<string>();
175
+ const path: string[] = [from, to];
176
+
177
+ function dfs(current: string): string[] | null {
178
+ if (current === from) {
179
+ // Found a cycle: return the path leading back to `from`
180
+ return path;
181
+ }
182
+ if (visited.has(current)) return null;
183
+ visited.add(current);
143
184
 
144
- // The use() function handles both loaders and handles
145
- ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
146
- // Handle case: return a push function
147
- if (isHandle(item)) {
148
- const handle = item;
149
- const store = getHandleStore();
150
- const segmentId = (ctx as InternalHandlerContext)._currentSegmentId;
185
+ const deps = dependsOn.get(current);
186
+ if (!deps) return null;
151
187
 
152
- if (!segmentId) {
153
- throw new Error(
154
- `Handle "${handle.$$id}" used outside of handler context. ` +
155
- `Handles must be used within route/layout handlers.`
156
- );
157
- }
188
+ for (const dep of deps) {
189
+ path.push(dep);
190
+ const cycle = dfs(dep);
191
+ if (cycle) return cycle;
192
+ path.pop();
193
+ }
194
+ return null;
195
+ }
158
196
 
159
- // Return a push function bound to this handle and segment
160
- // Accepts: value, Promise, or async callback (executed immediately)
161
- // Promises are pushed directly - RSC will serialize and stream them
162
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
163
- if (!store) return;
197
+ return dfs(to);
198
+ }
164
199
 
165
- // If it's a function, call it immediately to get the promise
166
- const valueOrPromise = typeof dataOrFn === "function"
167
- ? (dataOrFn as () => Promise<unknown>)()
168
- : dataOrFn;
200
+ /**
201
+ * Creates a memoizing loader executor with cycle detection.
202
+ * Shared by setupLoaderAccess and setupLoaderAccessSilent; only the handle
203
+ * branch differs between the two, so only the loader logic is extracted here.
204
+ *
205
+ * Returns a useLoader(loader, callerLoaderId) function that:
206
+ * - Tracks dependency edges between loaders for cycle detection
207
+ * - Throws immediately (synchronously inside an async fn) on circular deps
208
+ * - Memoizes each loader's promise so it runs at most once per request
209
+ */
210
+ function createLoaderExecutor<TEnv>(
211
+ ctx: HandlerContext<any, TEnv>,
212
+ loaderPromises: Map<string, Promise<any>>,
213
+ ): (
214
+ loader: LoaderDefinition<any, any>,
215
+ callerLoaderId: string | null,
216
+ ) => Promise<any> {
217
+ // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
218
+ const reqCtxRef = _getRequestContext();
219
+
220
+ // Dependency graph: loaderId -> set of loader IDs it directly depends on.
221
+ const dependsOn = new Map<string, Set<string>>();
222
+
223
+ // Loaders whose promises have not yet settled.
224
+ // A dependency on a pending loader that closes a cycle means deadlock.
225
+ const pendingLoaders = new Set<string>();
226
+
227
+ function useLoader(
228
+ loader: LoaderDefinition<any, any>,
229
+ callerLoaderId: string | null,
230
+ ): Promise<any> {
231
+ // Record the dependency edge and check for cycles before running
232
+ if (callerLoaderId !== null) {
233
+ let deps = dependsOn.get(callerLoaderId);
234
+ if (!deps) {
235
+ deps = new Set();
236
+ dependsOn.set(callerLoaderId, deps);
237
+ }
169
238
 
170
- // Push directly - promises will be serialized by RSC and streamed
171
- store.push(handle.$$id, segmentId, valueOrPromise);
172
- };
173
- }
239
+ // Only relevant when the target is still pending (would deadlock)
240
+ if (pendingLoaders.has(loader.$$id)) {
241
+ const cycle = detectLoaderCycle(callerLoaderId, loader.$$id, dependsOn);
242
+ if (cycle) {
243
+ throw new Error(
244
+ `Circular loader dependency detected: ${cycle.join(" -> ")}. ` +
245
+ `Loaders cannot depend on each other in a cycle. ` +
246
+ `Refactor to break the circular dependency.`,
247
+ );
248
+ }
249
+ }
174
250
 
175
- // Loader case: existing behavior
176
- const loader = item as LoaderDefinition<any, any>;
251
+ deps.add(loader.$$id);
252
+ }
177
253
 
178
254
  // Return cached promise if already started
179
255
  if (loaderPromises.has(loader.$$id)) {
180
- return loaderPromises.get(loader.$$id);
256
+ return loaderPromises.get(loader.$$id)!;
181
257
  }
182
258
 
183
259
  // Get loader function - either from loader object or fetchable registry
184
- // Fetchable loaders store fn in registry (not on object) to avoid client bundling issues
185
260
  let loaderFn = loader.fn;
186
261
  if (!loaderFn) {
187
262
  const fetchable = getFetchableLoader(loader.$$id);
@@ -190,46 +265,281 @@ export function setupLoaderAccess<TEnv>(
190
265
  }
191
266
  }
192
267
 
193
- // Ensure loader has a function
194
268
  if (!loaderFn) {
195
269
  throw new Error(
196
- `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
270
+ `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`,
197
271
  );
198
272
  }
199
273
 
200
- // Create loader context with recursive use() support
274
+ pendingLoaders.add(loader.$$id);
275
+
276
+ const currentLoaderId = loader.$$id;
277
+ const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
278
+
279
+ // Capture whether this loader is being started from a DSL loader scope
280
+ // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
281
+ // inside loader scope. This determines whether rendered() is allowed.
282
+ const isDslLoader = isInsideLoaderScope();
283
+
284
+ let renderedResolved = false;
285
+ let renderedPromise: Promise<void> | null = null;
286
+
287
+ // Loader functions are always fresh (never cached), so they get an
288
+ // unguarded get that bypasses non-cacheable read guards. This applies
289
+ // to ALL loaders — DSL and handler-called — because the loader
290
+ // function itself always re-executes. Also handles nested deps
291
+ // (loaderA → use(loaderB)) since all share this unguarded get.
201
292
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
202
293
  params: ctx.params,
294
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
203
295
  request: ctx.request,
204
296
  searchParams: ctx.searchParams,
297
+ search: (ctx as any).search,
205
298
  pathname: ctx.pathname,
206
299
  url: ctx.url,
300
+ originalUrl: ctx.originalUrl,
207
301
  env: ctx.env,
208
- var: ctx.var,
209
- get: ctx.get,
210
- use: <TDep, TDepParams = any>(
211
- dep: LoaderDefinition<TDep, TDepParams>
212
- ): Promise<TDep> => {
213
- // Recursive call - will start dep loader if not already started
214
- return ctx.use(dep);
215
- },
216
- // Default to GET for loaders called through route handlers
302
+ waitUntil: ctx.waitUntil.bind(ctx),
303
+ executionContext: ctx.executionContext,
304
+ get: ((keyOrVar: any) =>
305
+ contextGet(variables, keyOrVar)) as typeof ctx.get,
306
+ use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
307
+ if (isHandle(item)) {
308
+ if (!renderedResolved) {
309
+ throw new Error(
310
+ `ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
311
+ `Handle "${item.$$id}" cannot be read until the render tree has settled.`,
312
+ );
313
+ }
314
+ const reqCtx = reqCtxRef ?? _getRequestContext();
315
+ if (!reqCtx) {
316
+ throw new Error(
317
+ `ctx.use(handle) failed: request context not available.`,
318
+ );
319
+ }
320
+ const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
321
+ // The complete snapshot is cached at barrier resolution for
322
+ // non-streaming trees, and by rendered() after handleStore.settled for
323
+ // streaming trees (where the eager snapshot would have been incomplete
324
+ // because loading() handlers were still in flight). Either way it is
325
+ // present by the time a loader reads a handle; the fresh build is only
326
+ // a defensive fallback.
327
+ const snapshot =
328
+ reqCtx._renderBarrierHandleSnapshot ??
329
+ buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
330
+ return collectHandleData(item, snapshot, segmentOrder);
331
+ }
332
+
333
+ // Loader case
334
+ return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
335
+ }) as LoaderContext["use"],
217
336
  method: "GET",
218
337
  body: undefined,
338
+ reverse: ctx.reverse as LoaderContext["reverse"],
339
+ rendered: (): Promise<void> => {
340
+ // Guard: only DSL loaders may use rendered()
341
+ if (!isDslLoader) {
342
+ throw new Error(
343
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
344
+ `Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
345
+ );
346
+ }
347
+
348
+ const reqCtx = reqCtxRef ?? _getRequestContext();
349
+
350
+ if (renderedPromise) return renderedPromise;
351
+
352
+ if (!reqCtx) {
353
+ throw new Error(
354
+ `ctx.rendered() failed: request context not available.`,
355
+ );
356
+ }
357
+
358
+ // Bidirectional deadlock check: if a handler already started
359
+ // awaiting this loader, calling rendered() would deadlock. This is the
360
+ // real cycle guard (it holds for both streaming and non-streaming): the
361
+ // handler blocks segment resolution, which blocks the barrier, which
362
+ // blocks this loader.
363
+ if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
364
+ throw new Error(
365
+ `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
366
+ `is already awaiting this loader via ctx.use(). The handler blocks ` +
367
+ `segment resolution, which blocks the barrier, which blocks this loader. ` +
368
+ `Move the data dependency to a loader-to-loader pattern instead.`,
369
+ );
370
+ }
371
+
372
+ // Register this loader as waiting for the barrier so that
373
+ // setupLoaderAccess can detect deadlocks when a handler
374
+ // tries to await the same loader via ctx.use().
375
+ if (!reqCtx._renderBarrierWaiters) {
376
+ reqCtx._renderBarrierWaiters = new Set();
377
+ }
378
+ reqCtx._renderBarrierWaiters.add(currentLoaderId);
379
+
380
+ // Streaming trees (loading()): the barrier resolves once the segment
381
+ // tree is resolved, but loading() handlers stream behind Suspense and
382
+ // their handle pushes are still in flight then. Their async execution
383
+ // IS tracked in the handle store (trackHandler -> store.track), so after
384
+ // the barrier we seal (no further handlers register once the tree is
385
+ // resolved) and wait for settled — every tracked handler, streaming
386
+ // included, has finished pushing. The loader's own segment streams in
387
+ // after, so this does not block the shell; the deadlock guard above
388
+ // keeps a handler from depending on this loader.
389
+ const streaming = reqCtx._treeHasStreaming === true;
390
+ renderedPromise = reqCtx._renderBarrier.then(async () => {
391
+ if (streaming) {
392
+ reqCtx._handleStore.seal();
393
+ await reqCtx._handleStore.settled;
394
+ // The eager snapshot was intentionally left unbuilt for streaming
395
+ // (it would have been incomplete). Build the complete one once, now
396
+ // that the store has settled, so every ctx.use(handle) reads the
397
+ // cached snapshot instead of rebuilding it per call.
398
+ reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
399
+ reqCtx._handleStore,
400
+ reqCtx._renderBarrierSegmentOrder ?? [],
401
+ );
402
+ }
403
+ renderedResolved = true;
404
+ });
405
+ return renderedPromise;
406
+ },
219
407
  };
220
408
 
221
- // Start loader execution with tracking
222
- const doneLoader = track(`loader:${loader.$$id}`);
223
- const promise = Promise.resolve(
224
- loaderFn(loaderCtx as LoaderContext<any, TEnv>)
225
- ).finally(() => {
226
- doneLoader();
227
- });
409
+ // Meter this loader once via observePhase (loader:<id> perf metric +
410
+ // rango.loader span); loaderFn runs inside the span callback so its KV/D1/
411
+ // fetch spans nest under it. This is one of the observePhase loader funnels —
412
+ // see instrument.ts for the single-metering contract.
413
+ //
414
+ // Run the loader body inside loader scope so request-scoped reads
415
+ // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
416
+ // cache-purity guards: loaders always run fresh, so their reads never leak
417
+ // into a cached segment. DSL loaders are already wrapped by fresh.ts; this
418
+ // also covers handler-invoked loaders (ctx.use(Loader) from a handler),
419
+ // which otherwise execute in the caller's cache scope and would wrongly
420
+ // throw. rendered() gating uses the captured isDslLoader (above), so this
421
+ // does not grant rendered() to handler-invoked loaders. Uses a body-only
422
+ // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
423
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
424
+ Promise.resolve(
425
+ runInsideLoaderBodyScope(() =>
426
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
427
+ ),
428
+ ).finally(() => {
429
+ pendingLoaders.delete(loader.$$id);
430
+ }),
431
+ );
228
432
 
229
- // Memoize for subsequent calls
230
433
  loaderPromises.set(loader.$$id, promise);
231
-
232
434
  return promise;
435
+ }
436
+
437
+ return useLoader;
438
+ }
439
+
440
+ /**
441
+ * Set up the use() method on handler context to access loaders and handles.
442
+ *
443
+ * For loaders: Lazily runs loaders, memoizes results per request.
444
+ * For handles: Returns a push function bound to the current segment.
445
+ *
446
+ * Includes cycle detection: tracks dependency edges between loaders and
447
+ * throws on circular dependencies to prevent deadlocks.
448
+ */
449
+ export function setupLoaderAccess<TEnv>(
450
+ ctx: HandlerContext<any, TEnv>,
451
+ loaderPromises: Map<string, Promise<any>>,
452
+ ): void {
453
+ // Eagerly capture the request context and HandleStore at setup time
454
+ // (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
455
+ // fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
456
+ // getRequestContext() to return undefined when handlers later call
457
+ // ctx.use(handle). Capturing early ensures references survive ALS disruption.
458
+ const reqCtxRef = _getRequestContext();
459
+ const handleStoreRef = reqCtxRef?._handleStore;
460
+
461
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
462
+
463
+ ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
464
+ if (isHandle(item)) {
465
+ const handle = item;
466
+ const store = handleStoreRef;
467
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
468
+ ._currentSegmentId;
469
+
470
+ if (!segmentId) {
471
+ throw new Error(
472
+ `Handle "${handle.$$id}" used outside of handler context. ` +
473
+ `Handles must be used within route/layout handlers.`,
474
+ );
475
+ }
476
+
477
+ return withDefer(
478
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
479
+ if (!store) return;
480
+
481
+ if (typeof dataOrFn === "function") {
482
+ // Run the callback inside the push-callback scope so ctx.use(loader)
483
+ // calls it makes — including after its own awaits, for an async
484
+ // callback — are not registered as handler-to-loader deps and do not
485
+ // trip the deadlock guard. A pushed promise value is not tracked by
486
+ // handleStore.settled and does not block segment resolution, so it
487
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
488
+ // boolean) is what survives the callback's awaits.
489
+ const result = runInsidePushCallbackScope(() =>
490
+ (dataOrFn as () => Promise<unknown>)(),
491
+ );
492
+ store.push(handle.$$id, segmentId, result);
493
+ return;
494
+ }
495
+
496
+ store.push(handle.$$id, segmentId, dataOrFn);
497
+ },
498
+ );
499
+ }
500
+
501
+ // Deadlock guard and handler-to-loader dependency tracking.
502
+ // Skip when inside a DSL loader scope (resolveLoaderData also calls
503
+ // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
504
+ // inside a handle push callback (push callbacks don't block segment
505
+ // resolution so they can't cause rendered() deadlocks). The push-callback
506
+ // check is an ALS scope so it also exempts an ASYNC callback's continuation
507
+ // after its first await — relevant on streaming trees, where the guard
508
+ // state now stays live until handleStore.settled.
509
+ const loader = item as LoaderDefinition<any, any>;
510
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
511
+ const reqCtx = reqCtxRef ?? _getRequestContext();
512
+ if (reqCtx) {
513
+ // Direction 1: handler awaits loader that already called rendered()
514
+ if (
515
+ loaderPromises.has(loader.$$id) &&
516
+ reqCtx._renderBarrierWaiters?.has(loader.$$id)
517
+ ) {
518
+ throw new Error(
519
+ `Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
520
+ `The loader is waiting for segment resolution, but the handler blocks resolution. ` +
521
+ `Move the data dependency to a loader-to-loader pattern instead.`,
522
+ );
523
+ }
524
+ // Direction 2: track dep so rendered() can detect the deadlock if the
525
+ // loader calls it later. Skip once the guard window is CLOSED — for a
526
+ // non-streaming tree that is when the barrier resolves (rendered()
527
+ // resolves immediately), and for a streaming tree it is when
528
+ // handleStore.settled completes (rendered() keeps waiting until then, so
529
+ // a loading() handler resuming after the barrier can still form a
530
+ // cycle). Using the explicit guard-closed flag rather than
531
+ // _renderBarrierSegmentOrder keeps tracking live across the streaming
532
+ // settle wait. (Handle push callbacks are already excluded above via
533
+ // isInsidePushCallbackScope(), so they cannot produce false positives
534
+ // here.)
535
+ if (!reqCtx._renderBarrierGuardClosed) {
536
+ if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
537
+ reqCtx._handlerLoaderDeps.add(loader.$$id);
538
+ }
539
+ }
540
+ }
541
+
542
+ return useLoader(loader, null);
233
543
  }) as typeof ctx.use;
234
544
  }
235
545
 
@@ -237,20 +547,17 @@ export function setupLoaderAccess<TEnv>(
237
547
  * Set up ctx.use() for pre-rendering (build-time).
238
548
  * Handles push to HandleStore; loaders throw with a clear error.
239
549
  */
240
- export function setupBuildUse<TEnv>(
241
- ctx: HandlerContext<any, TEnv>,
242
- ): void {
243
- // Get HandleStore from request context
244
- const getHandleStore = (): HandleStore | undefined => {
245
- return getRequestContext()?._handleStore;
246
- };
550
+ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
551
+ // Eagerly capture the HandleStore (same ALS protection as setupLoaderAccess).
552
+ const handleStoreRef = _getRequestContext()?._handleStore;
247
553
 
248
554
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
249
555
  // Handle case: return a push function bound to the current segment
250
556
  if (isHandle(item)) {
251
557
  const handle = item;
252
- const store = getHandleStore();
253
- const segmentId = (ctx as InternalHandlerContext)._currentSegmentId;
558
+ const store = handleStoreRef;
559
+ const segmentId = (ctx as InternalHandlerContext<any, TEnv>)
560
+ ._currentSegmentId;
254
561
 
255
562
  if (!segmentId) {
256
563
  throw new Error(
@@ -259,15 +566,21 @@ export function setupBuildUse<TEnv>(
259
566
  );
260
567
  }
261
568
 
262
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
263
- if (!store) return;
569
+ // Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
570
+ // prerender path, matching production setupLoaderAccess. Without it a
571
+ // prerender handler calling .defer() throws "defer is not a function".
572
+ return withDefer(
573
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
574
+ if (!store) return;
264
575
 
265
- const valueOrPromise = typeof dataOrFn === "function"
266
- ? (dataOrFn as () => Promise<unknown>)()
267
- : dataOrFn;
576
+ const valueOrPromise =
577
+ typeof dataOrFn === "function"
578
+ ? (dataOrFn as () => Promise<unknown>)()
579
+ : dataOrFn;
268
580
 
269
- store.push(handle.$$id, segmentId, valueOrPromise);
270
- };
581
+ store.push(handle.$$id, segmentId, valueOrPromise);
582
+ },
583
+ );
271
584
  }
272
585
 
273
586
  // Loader case: not available during pre-rendering
@@ -282,76 +595,27 @@ export function setupBuildUse<TEnv>(
282
595
  /**
283
596
  * Set up ctx.use() for proactive caching (silent mode).
284
597
  * Handles are silently ignored (no push to HandleStore).
285
- * Loaders work normally but with fresh memoization.
598
+ * Loaders work normally but with fresh memoization and cycle detection.
286
599
  *
287
600
  * This prevents duplicate handle data (breadcrumbs, meta) from being
288
601
  * pushed to the response stream during background proactive caching.
289
602
  */
290
603
  export function setupLoaderAccessSilent<TEnv>(
291
604
  ctx: HandlerContext<any, TEnv>,
292
- loaderPromises: Map<string, Promise<any>>
605
+ loaderPromises: Map<string, Promise<any>>,
293
606
  ): void {
607
+ const useLoader = createLoaderExecutor(ctx, loaderPromises);
608
+
294
609
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
295
- // Handle case: return a no-op push function
296
610
  if (isHandle(item)) {
297
- // Silent mode - return a function that does nothing
298
- return (_dataOrFn: unknown) => {
299
- // Intentionally empty - don't push handle data during proactive caching
300
- };
301
- }
302
-
303
- // Loader case: same as setupLoaderAccess
304
- const loader = item as LoaderDefinition<any, any>;
305
-
306
- // Return cached promise if already started
307
- if (loaderPromises.has(loader.$$id)) {
308
- return loaderPromises.get(loader.$$id);
611
+ // Silent mode - return a no-op so handle data is not pushed during caching.
612
+ // Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
613
+ // callable resolver (also a no-op here), matching production's push shape
614
+ // instead of throwing "defer is not a function".
615
+ return withDefer((_dataOrFn: unknown) => {});
309
616
  }
310
617
 
311
- // Get loader function
312
- let loaderFn = loader.fn;
313
- if (!loaderFn) {
314
- const fetchable = getFetchableLoader(loader.$$id);
315
- if (fetchable) {
316
- loaderFn = fetchable.fn;
317
- }
318
- }
319
-
320
- if (!loaderFn) {
321
- throw new Error(
322
- `Loader "${loader.$$id}" has no function. This usually means the loader was defined without "use server" and the function was not included in the build.`
323
- );
324
- }
325
-
326
- // Create loader context with recursive use() support
327
- const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
328
- params: ctx.params,
329
- request: ctx.request,
330
- searchParams: ctx.searchParams,
331
- pathname: ctx.pathname,
332
- url: ctx.url,
333
- env: ctx.env,
334
- var: ctx.var,
335
- get: ctx.get,
336
- use: <TDep, TDepParams = any>(
337
- dep: LoaderDefinition<TDep, TDepParams>
338
- ): Promise<TDep> => {
339
- return ctx.use(dep);
340
- },
341
- method: "GET",
342
- body: undefined,
343
- };
344
-
345
- // Start loader execution with tracking
346
- const doneLoader = track(`loader:${loader.$$id}`);
347
- const promise = Promise.resolve(
348
- loaderFn(loaderCtx as LoaderContext<any, TEnv>)
349
- ).finally(() => {
350
- doneLoader();
351
- });
352
-
353
- loaderPromises.set(loader.$$id, promise);
354
- return promise;
618
+ return useLoader(item as LoaderDefinition<any, any>, null);
355
619
  }) as typeof ctx.use;
356
620
  }
357
621
 
@@ -367,7 +631,7 @@ export function setupLoaderAccessSilent<TEnv>(
367
631
  export async function revalidate<T>(
368
632
  shouldRevalidate: () => Promise<boolean>,
369
633
  onRevalidate: () => Promise<T>,
370
- onSkip: () => T
634
+ onSkip: () => T,
371
635
  ): Promise<T> {
372
636
  const needsRevalidation = await shouldRevalidate();
373
637
  return needsRevalidation ? await onRevalidate() : onSkip();