@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327

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 (440) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +301 -797
  3. package/dist/bin/rango.js +603 -145
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3750 -1160
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +96 -24
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +85 -6
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +228 -33
  12. package/skills/caching/SKILL.md +336 -19
  13. package/skills/catalog.json +271 -0
  14. package/skills/comparison/SKILL.md +50 -0
  15. package/skills/comparison/agents/openai.yaml +4 -0
  16. package/skills/comparison/references/framework-comparison.md +837 -0
  17. package/skills/composability/SKILL.md +110 -4
  18. package/skills/css/SKILL.md +76 -0
  19. package/skills/debug-manifest/SKILL.md +5 -3
  20. package/skills/defer-hydration/SKILL.md +235 -0
  21. package/skills/document-cache/SKILL.md +87 -56
  22. package/skills/fonts/SKILL.md +1 -1
  23. package/skills/handler-use/SKILL.md +12 -10
  24. package/skills/hooks/SKILL.md +73 -691
  25. package/skills/hooks/data.md +273 -0
  26. package/skills/hooks/handle-and-actions.md +103 -0
  27. package/skills/hooks/navigation.md +110 -0
  28. package/skills/hooks/outlets.md +41 -0
  29. package/skills/hooks/state.md +228 -0
  30. package/skills/hooks/urls.md +135 -0
  31. package/skills/host-router/SKILL.md +129 -27
  32. package/skills/i18n/SKILL.md +276 -0
  33. package/skills/intercept/SKILL.md +75 -19
  34. package/skills/layout/SKILL.md +40 -19
  35. package/skills/links/SKILL.md +247 -17
  36. package/skills/loader/SKILL.md +248 -10
  37. package/skills/middleware/SKILL.md +25 -13
  38. package/skills/migrate-nextjs/SKILL.md +205 -20
  39. package/skills/migrate-react-router/SKILL.md +59 -670
  40. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  41. package/skills/migrate-react-router/component-migration.md +196 -0
  42. package/skills/migrate-react-router/data-and-actions.md +225 -0
  43. package/skills/migrate-react-router/route-mapping.md +271 -0
  44. package/skills/mime-routes/SKILL.md +29 -2
  45. package/skills/observability/SKILL.md +202 -0
  46. package/skills/parallel/SKILL.md +40 -10
  47. package/skills/ppr/SKILL.md +616 -0
  48. package/skills/prerender/SKILL.md +72 -60
  49. package/skills/rango/SKILL.md +318 -26
  50. package/skills/react-compiler/SKILL.md +168 -0
  51. package/skills/response-routes/SKILL.md +138 -49
  52. package/skills/route/SKILL.md +117 -9
  53. package/skills/router-setup/SKILL.md +44 -9
  54. package/skills/scripts/SKILL.md +179 -0
  55. package/skills/server-actions/SKILL.md +776 -0
  56. package/skills/shell-manifest/SKILL.md +185 -0
  57. package/skills/streams-and-websockets/SKILL.md +283 -0
  58. package/skills/tailwind/SKILL.md +28 -4
  59. package/skills/testing/SKILL.md +130 -0
  60. package/skills/testing/bindings.md +103 -0
  61. package/skills/testing/cache-prerender.md +127 -0
  62. package/skills/testing/client-components.md +124 -0
  63. package/skills/testing/e2e-parity.md +125 -0
  64. package/skills/testing/flight.md +91 -0
  65. package/skills/testing/handles.md +131 -0
  66. package/skills/testing/loader.md +128 -0
  67. package/skills/testing/middleware.md +99 -0
  68. package/skills/testing/render-handler.md +122 -0
  69. package/skills/testing/response-routes.md +95 -0
  70. package/skills/testing/reverse-and-types.md +85 -0
  71. package/skills/testing/server-actions.md +107 -0
  72. package/skills/testing/server-tree.md +128 -0
  73. package/skills/testing/setup.md +123 -0
  74. package/skills/theme/SKILL.md +1 -1
  75. package/skills/typesafety/SKILL.md +45 -626
  76. package/skills/typesafety/env-and-bindings.md +254 -0
  77. package/skills/typesafety/generated-files-and-cli.md +335 -0
  78. package/skills/typesafety/params-and-search.md +153 -0
  79. package/skills/typesafety/route-types.md +209 -0
  80. package/skills/use-cache/SKILL.md +74 -15
  81. package/skills/vercel/SKILL.md +128 -0
  82. package/skills/view-transitions/SKILL.md +337 -0
  83. package/src/__augment-tests__/augment.ts +81 -0
  84. package/src/__augment-tests__/augmented.check.ts +116 -0
  85. package/src/__internal.ts +0 -65
  86. package/src/browser/action-coordinator.ts +53 -36
  87. package/src/browser/action-fence.ts +47 -0
  88. package/src/browser/app-shell.ts +39 -0
  89. package/src/browser/connection-warmup.ts +134 -0
  90. package/src/browser/cookie-name.ts +140 -0
  91. package/src/browser/event-controller.ts +252 -158
  92. package/src/browser/history-state.ts +21 -0
  93. package/src/browser/index.ts +3 -3
  94. package/src/browser/invalidate-client-cache.ts +52 -0
  95. package/src/browser/logging.ts +28 -0
  96. package/src/browser/merge-segment-loaders.ts +6 -4
  97. package/src/browser/navigation-bridge.ts +94 -25
  98. package/src/browser/navigation-client.ts +144 -79
  99. package/src/browser/navigation-store-handle.ts +38 -0
  100. package/src/browser/navigation-store.ts +161 -73
  101. package/src/browser/navigation-transaction.ts +9 -59
  102. package/src/browser/network-error-handler.ts +34 -7
  103. package/src/browser/partial-update.ts +183 -144
  104. package/src/browser/prefetch/cache.ts +242 -77
  105. package/src/browser/prefetch/fetch.ts +325 -69
  106. package/src/browser/prefetch/queue.ts +61 -12
  107. package/src/browser/rango-state.ts +158 -76
  108. package/src/browser/react/Link.tsx +58 -20
  109. package/src/browser/react/NavigationProvider.tsx +202 -120
  110. package/src/browser/react/ScrollRestoration.tsx +10 -6
  111. package/src/browser/react/filter-segment-order.ts +66 -7
  112. package/src/browser/react/index.ts +0 -48
  113. package/src/browser/react/location-state-shared.ts +178 -8
  114. package/src/browser/react/location-state.ts +39 -14
  115. package/src/browser/react/use-action.ts +6 -15
  116. package/src/browser/react/use-handle.ts +17 -14
  117. package/src/browser/react/use-href.tsx +8 -1
  118. package/src/browser/react/use-link-status.ts +33 -8
  119. package/src/browser/react/use-navigation.ts +32 -7
  120. package/src/browser/react/use-params.ts +20 -10
  121. package/src/browser/react/use-reverse.ts +106 -0
  122. package/src/browser/react/use-router.ts +25 -3
  123. package/src/browser/react/use-search-params.ts +0 -5
  124. package/src/browser/react/use-segments.ts +11 -21
  125. package/src/browser/response-adapter.ts +99 -8
  126. package/src/browser/rsc-router.tsx +145 -28
  127. package/src/browser/scroll-restoration.ts +37 -22
  128. package/src/browser/segment-reconciler.ts +31 -21
  129. package/src/browser/segment-structure-assert.ts +2 -2
  130. package/src/browser/server-action-bridge.ts +236 -65
  131. package/src/browser/types.ts +102 -9
  132. package/src/browser/validate-redirect-origin.ts +43 -16
  133. package/src/build/collect-fallback-refs.ts +107 -0
  134. package/src/build/generate-manifest.ts +203 -154
  135. package/src/build/generate-route-types.ts +3 -1
  136. package/src/build/index.ts +11 -3
  137. package/src/build/prefix-tree-utils.ts +123 -0
  138. package/src/build/route-trie.ts +152 -21
  139. package/src/build/route-types/ast-route-extraction.ts +15 -8
  140. package/src/build/route-types/codegen.ts +16 -5
  141. package/src/build/route-types/include-resolution.ts +456 -62
  142. package/src/build/route-types/param-extraction.ts +6 -3
  143. package/src/build/route-types/per-module-writer.ts +22 -6
  144. package/src/build/route-types/router-processing.ts +128 -51
  145. package/src/build/route-types/scan-filter.ts +1 -1
  146. package/src/build/route-types/source-scan.ts +216 -0
  147. package/src/build/runtime-discovery.ts +13 -21
  148. package/src/cache/cache-error.ts +104 -0
  149. package/src/cache/cache-key-utils.ts +58 -13
  150. package/src/cache/cache-policy.ts +108 -34
  151. package/src/cache/cache-runtime.ts +421 -58
  152. package/src/cache/cache-scope.ts +187 -96
  153. package/src/cache/cache-tag.ts +149 -0
  154. package/src/cache/cf/cf-base64.ts +33 -0
  155. package/src/cache/cf/cf-cache-constants.ts +127 -0
  156. package/src/cache/cf/cf-cache-store.ts +2202 -372
  157. package/src/cache/cf/cf-cache-types.ts +349 -0
  158. package/src/cache/cf/cf-kv-utils.ts +46 -0
  159. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  160. package/src/cache/cf/index.ts +6 -16
  161. package/src/cache/document-cache.ts +126 -41
  162. package/src/cache/handle-snapshot.ts +70 -0
  163. package/src/cache/index.ts +23 -20
  164. package/src/cache/memory-segment-store.ts +243 -37
  165. package/src/cache/profile-registry.ts +46 -31
  166. package/src/cache/read-through-swr.ts +56 -12
  167. package/src/cache/segment-codec.ts +13 -21
  168. package/src/cache/shell-snapshot.ts +417 -0
  169. package/src/cache/tag-invalidation.ts +230 -0
  170. package/src/cache/types.ts +180 -99
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1127 -0
  173. package/src/client.rsc.tsx +41 -21
  174. package/src/client.tsx +33 -61
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +108 -0
  177. package/src/component-utils.ts +19 -0
  178. package/src/components/DefaultDocument.tsx +8 -2
  179. package/src/context-var.ts +18 -6
  180. package/src/decode-loader-results.ts +52 -0
  181. package/src/defer.ts +185 -0
  182. package/src/deps/ssr.ts +0 -1
  183. package/src/encode-kv.ts +49 -0
  184. package/src/errors.ts +30 -4
  185. package/src/escape-script.ts +52 -0
  186. package/src/handle.ts +67 -37
  187. package/src/handles/MetaTags.tsx +24 -53
  188. package/src/handles/Scripts.tsx +183 -0
  189. package/src/handles/breadcrumbs.ts +35 -8
  190. package/src/handles/deferred-resolution.ts +127 -0
  191. package/src/handles/is-thenable.ts +18 -0
  192. package/src/handles/meta.ts +14 -40
  193. package/src/handles/script.ts +244 -0
  194. package/src/host/cookie-handler.ts +9 -60
  195. package/src/host/errors.ts +13 -22
  196. package/src/host/index.ts +9 -2
  197. package/src/host/pattern-matcher.ts +23 -52
  198. package/src/host/router.ts +107 -99
  199. package/src/host/testing.ts +40 -27
  200. package/src/host/types.ts +37 -4
  201. package/src/host/utils.ts +1 -1
  202. package/src/href-client.ts +137 -22
  203. package/src/index.rsc.ts +97 -12
  204. package/src/index.ts +98 -14
  205. package/src/internal-debug.ts +11 -10
  206. package/src/loader-store.ts +500 -0
  207. package/src/loader.rsc.ts +20 -13
  208. package/src/loader.ts +12 -11
  209. package/src/missing-id-error.ts +68 -0
  210. package/src/outlet-context.ts +1 -1
  211. package/src/outlet-provider.tsx +1 -5
  212. package/src/prerender/param-hash.ts +16 -16
  213. package/src/prerender/store.ts +32 -37
  214. package/src/prerender.ts +78 -10
  215. package/src/redirect-origin.ts +114 -0
  216. package/src/regex-escape.ts +8 -0
  217. package/src/render-error-thrower.tsx +20 -0
  218. package/src/response-utils.ts +62 -0
  219. package/src/reverse.ts +65 -39
  220. package/src/root-error-boundary.tsx +1 -19
  221. package/src/route-content-wrapper.tsx +19 -77
  222. package/src/route-definition/dsl-helpers.ts +304 -309
  223. package/src/route-definition/helper-factories.ts +28 -140
  224. package/src/route-definition/helpers-types.ts +87 -59
  225. package/src/route-definition/index.ts +1 -2
  226. package/src/route-definition/redirect.ts +44 -11
  227. package/src/route-definition/resolve-handler-use.ts +12 -1
  228. package/src/route-definition/use-item-types.ts +29 -0
  229. package/src/route-map-builder.ts +41 -20
  230. package/src/route-types.ts +19 -46
  231. package/src/router/basename.ts +14 -0
  232. package/src/router/content-negotiation.ts +73 -25
  233. package/src/router/error-handling.ts +45 -18
  234. package/src/router/find-match.ts +129 -30
  235. package/src/router/handler-context.ts +27 -42
  236. package/src/router/instrument.ts +355 -0
  237. package/src/router/intercept-resolution.ts +39 -20
  238. package/src/router/lazy-includes.ts +82 -59
  239. package/src/router/loader-resolution.ts +167 -72
  240. package/src/router/logging.ts +0 -6
  241. package/src/router/manifest.ts +74 -40
  242. package/src/router/match-api.ts +80 -55
  243. package/src/router/match-context.ts +0 -22
  244. package/src/router/match-handlers.ts +211 -165
  245. package/src/router/match-middleware/background-revalidation.ts +40 -24
  246. package/src/router/match-middleware/cache-lookup.ts +159 -285
  247. package/src/router/match-middleware/cache-store.ts +64 -52
  248. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  249. package/src/router/match-middleware/segment-resolution.ts +0 -22
  250. package/src/router/match-pipelines.ts +1 -42
  251. package/src/router/match-result.ts +69 -79
  252. package/src/router/metrics.ts +0 -34
  253. package/src/router/middleware-types.ts +7 -134
  254. package/src/router/middleware.ts +298 -172
  255. package/src/router/navigation-snapshot.ts +7 -56
  256. package/src/router/params-util.ts +23 -0
  257. package/src/router/parse-pattern.ts +115 -0
  258. package/src/router/pattern-matching.ts +181 -150
  259. package/src/router/prefetch-cache-ttl.ts +51 -0
  260. package/src/router/prefetch-limits.ts +37 -0
  261. package/src/router/prerender-match.ts +112 -67
  262. package/src/router/preview-match.ts +6 -2
  263. package/src/router/request-classification.ts +50 -69
  264. package/src/router/revalidation.ts +123 -73
  265. package/src/router/route-snapshot.ts +14 -3
  266. package/src/router/router-context.ts +6 -29
  267. package/src/router/router-interfaces.ts +115 -36
  268. package/src/router/router-options.ts +166 -5
  269. package/src/router/router-registry.ts +2 -5
  270. package/src/router/segment-resolution/fresh.ts +131 -86
  271. package/src/router/segment-resolution/helpers.ts +86 -6
  272. package/src/router/segment-resolution/loader-cache.ts +139 -39
  273. package/src/router/segment-resolution/loader-mask.ts +67 -0
  274. package/src/router/segment-resolution/loader-snapshot.ts +251 -0
  275. package/src/router/segment-resolution/revalidation.ts +272 -320
  276. package/src/router/segment-resolution/static-store.ts +19 -5
  277. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  278. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  279. package/src/router/segment-resolution.ts +5 -1
  280. package/src/router/segment-wrappers.ts +6 -5
  281. package/src/router/state-cookie-name.ts +33 -0
  282. package/src/router/substitute-pattern-params.ts +75 -0
  283. package/src/router/telemetry-otel.ts +160 -200
  284. package/src/router/telemetry.ts +105 -20
  285. package/src/router/timeout.ts +0 -20
  286. package/src/router/tracing.ts +215 -0
  287. package/src/router/trie-matching.ts +171 -59
  288. package/src/router/types.ts +9 -63
  289. package/src/router/url-params.ts +57 -0
  290. package/src/router.ts +157 -71
  291. package/src/rsc/full-payload.ts +70 -0
  292. package/src/rsc/handler-context.ts +3 -2
  293. package/src/rsc/handler.ts +291 -217
  294. package/src/rsc/helpers.ts +168 -46
  295. package/src/rsc/index.ts +2 -5
  296. package/src/rsc/json-route-result.ts +38 -0
  297. package/src/rsc/loader-fetch.ts +114 -38
  298. package/src/rsc/manifest-init.ts +29 -42
  299. package/src/rsc/nonce.ts +10 -1
  300. package/src/rsc/origin-guard.ts +39 -25
  301. package/src/rsc/progressive-enhancement.ts +124 -13
  302. package/src/rsc/redirect-guard.ts +100 -0
  303. package/src/rsc/response-cache-serve.ts +238 -0
  304. package/src/rsc/response-error.ts +79 -12
  305. package/src/rsc/response-route-handler.ts +99 -189
  306. package/src/rsc/rsc-rendering.ts +421 -76
  307. package/src/rsc/runtime-warnings.ts +23 -10
  308. package/src/rsc/server-action.ts +282 -116
  309. package/src/rsc/shell-capture.ts +1158 -0
  310. package/src/rsc/shell-serve.ts +150 -0
  311. package/src/rsc/ssr-setup.ts +16 -0
  312. package/src/rsc/transition-gate.ts +89 -0
  313. package/src/rsc/types.ts +53 -5
  314. package/src/runtime-env.ts +18 -0
  315. package/src/search-params.ts +35 -30
  316. package/src/segment-loader-promise.ts +49 -4
  317. package/src/segment-system.tsx +350 -149
  318. package/src/serialize.ts +243 -0
  319. package/src/server/context.ts +208 -51
  320. package/src/server/cookie-parse.ts +32 -0
  321. package/src/server/cookie-store.ts +152 -5
  322. package/src/server/handle-store.ts +21 -38
  323. package/src/server/loader-registry.ts +33 -42
  324. package/src/server/request-context.ts +395 -176
  325. package/src/ssr/index.tsx +458 -178
  326. package/src/ssr/ssr-root.tsx +228 -0
  327. package/src/static-handler.ts +10 -13
  328. package/src/testing/cache-status.ts +162 -0
  329. package/src/testing/collect-handle.ts +46 -0
  330. package/src/testing/dispatch.ts +813 -0
  331. package/src/testing/dom.entry.ts +22 -0
  332. package/src/testing/e2e/fixture.ts +188 -0
  333. package/src/testing/e2e/index.ts +128 -0
  334. package/src/testing/e2e/matchers.ts +35 -0
  335. package/src/testing/e2e/page-helpers.ts +272 -0
  336. package/src/testing/e2e/parity.ts +387 -0
  337. package/src/testing/e2e/server.ts +195 -0
  338. package/src/testing/flight-matchers.ts +97 -0
  339. package/src/testing/flight-normalize.ts +11 -0
  340. package/src/testing/flight-runtime.d.ts +57 -0
  341. package/src/testing/flight-tree.ts +682 -0
  342. package/src/testing/flight.entry.ts +52 -0
  343. package/src/testing/flight.ts +257 -0
  344. package/src/testing/generated-routes.ts +199 -0
  345. package/src/testing/index.ts +105 -0
  346. package/src/testing/internal/context.ts +371 -0
  347. package/src/testing/internal/flight-client-globals.ts +30 -0
  348. package/src/testing/internal/seed-vars.ts +54 -0
  349. package/src/testing/render-handler.ts +357 -0
  350. package/src/testing/render-route.tsx +584 -0
  351. package/src/testing/run-loader.ts +385 -0
  352. package/src/testing/run-middleware.ts +205 -0
  353. package/src/testing/run-transition-when.ts +164 -0
  354. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  355. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  356. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  357. package/src/testing/vitest-stubs/version.ts +5 -0
  358. package/src/testing/vitest.ts +305 -0
  359. package/src/theme/ThemeProvider.tsx +56 -84
  360. package/src/theme/ThemeScript.tsx +7 -9
  361. package/src/theme/constants.ts +52 -13
  362. package/src/theme/index.ts +0 -7
  363. package/src/theme/theme-context.ts +1 -5
  364. package/src/theme/theme-script.ts +22 -21
  365. package/src/theme/use-theme.ts +0 -3
  366. package/src/types/boundaries.ts +0 -35
  367. package/src/types/cache-types.ts +13 -4
  368. package/src/types/error-types.ts +30 -90
  369. package/src/types/global-namespace.ts +54 -41
  370. package/src/types/handler-context.ts +110 -62
  371. package/src/types/index.ts +3 -10
  372. package/src/types/loader-types.ts +11 -9
  373. package/src/types/request-scope.ts +112 -0
  374. package/src/types/route-config.ts +20 -52
  375. package/src/types/route-entry.ts +0 -6
  376. package/src/types/segments.ts +135 -14
  377. package/src/urls/include-helper.ts +19 -64
  378. package/src/urls/include-provider.ts +71 -0
  379. package/src/urls/index.ts +2 -11
  380. package/src/urls/path-helper-types.ts +63 -17
  381. package/src/urls/path-helper.ts +22 -106
  382. package/src/urls/pattern-types.ts +72 -19
  383. package/src/urls/response-types.ts +22 -29
  384. package/src/urls/type-extraction.ts +98 -154
  385. package/src/urls/urls-function.ts +1 -19
  386. package/src/use-loader.tsx +292 -107
  387. package/src/vercel/index.ts +11 -0
  388. package/src/vercel/tracing.ts +88 -0
  389. package/src/vite/debug.ts +185 -0
  390. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  391. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  392. package/src/vite/discovery/discover-routers.ts +127 -86
  393. package/src/vite/discovery/discovery-errors.ts +255 -0
  394. package/src/vite/discovery/gate-state.ts +171 -0
  395. package/src/vite/discovery/prerender-collection.ts +96 -68
  396. package/src/vite/discovery/route-types-writer.ts +40 -84
  397. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  398. package/src/vite/discovery/state.ts +45 -1
  399. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  400. package/src/vite/index.ts +4 -0
  401. package/src/vite/inject-client-debug.ts +88 -0
  402. package/src/vite/plugin-types.ts +210 -10
  403. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  404. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  405. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  406. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  407. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  408. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  409. package/src/vite/plugins/expose-action-id.ts +48 -95
  410. package/src/vite/plugins/expose-id-utils.ts +88 -55
  411. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  412. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  413. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  414. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  415. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  416. package/src/vite/plugins/performance-tracks.ts +26 -25
  417. package/src/vite/plugins/refresh-cmd.ts +1 -1
  418. package/src/vite/plugins/use-cache-transform.ts +73 -83
  419. package/src/vite/plugins/vercel-output.ts +384 -0
  420. package/src/vite/plugins/version-injector.ts +40 -29
  421. package/src/vite/plugins/version-plugin.ts +37 -40
  422. package/src/vite/plugins/virtual-entries.ts +138 -27
  423. package/src/vite/rango.ts +236 -138
  424. package/src/vite/router-discovery.ts +927 -136
  425. package/src/vite/utils/ast-handler-extract.ts +26 -35
  426. package/src/vite/utils/banner.ts +1 -1
  427. package/src/vite/utils/bundle-analysis.ts +10 -15
  428. package/src/vite/utils/client-chunks.ts +184 -0
  429. package/src/vite/utils/directive-prologue.ts +40 -0
  430. package/src/vite/utils/forward-user-plugins.ts +171 -0
  431. package/src/vite/utils/manifest-utils.ts +4 -59
  432. package/src/vite/utils/package-resolution.ts +20 -52
  433. package/src/vite/utils/prerender-utils.ts +71 -43
  434. package/src/vite/utils/shared-utils.ts +142 -43
  435. package/src/browser/action-response-classifier.ts +0 -99
  436. package/src/browser/react/use-client-cache.ts +0 -58
  437. package/src/browser/shallow.ts +0 -40
  438. package/src/handles/index.ts +0 -7
  439. package/src/network-error-thrower.tsx +0 -23
  440. package/src/router/middleware-cookies.ts +0 -55
@@ -1,14 +1,18 @@
1
1
  import { registerRouteMap } from "../route-map-builder.js";
2
- import { extractStaticPrefix } from "./pattern-matching.js";
2
+ import { extractStaticPrefix, joinPrefix } from "./pattern-matching.js";
3
3
  import {
4
- EntryData,
5
- RSCRouterContext,
4
+ type EntryData,
5
+ RangoContext,
6
6
  runWithPrefixes,
7
7
  getIsolatedLazyParent,
8
8
  } from "../server/context";
9
9
  import type { UrlPatterns } from "../urls.js";
10
10
  import type { AllUseItems, IncludeItem } from "../route-types.js";
11
11
  import type { ResolvedRouteMap, RouteEntry, TrailingSlashMode } from "../types";
12
+ import {
13
+ isIncludeProvider,
14
+ resolveIncludeModule,
15
+ } from "../urls/include-provider.js";
12
16
 
13
17
  export interface LazyEvalDeps<TEnv = any> {
14
18
  routesEntries: RouteEntry<TEnv>[];
@@ -18,9 +22,6 @@ export interface LazyEvalDeps<TEnv = any> {
18
22
  routerId?: string;
19
23
  }
20
24
 
21
- // Detect lazy includes in handler result and create placeholder entries
22
- // Lazy includes are IncludeItem with lazy: true and _lazyContext
23
- // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
24
25
  export function findLazyIncludes<TEnv = any>(
25
26
  items: AllUseItems[],
26
27
  ): Array<{
@@ -56,7 +57,6 @@ export function findLazyIncludes<TEnv = any>(
56
57
  });
57
58
  }
58
59
  }
59
- // Recursively check nested items (in layouts, etc.)
60
60
  if ((item as any).uses && Array.isArray((item as any).uses)) {
61
61
  lazyItems.push(...findLazyIncludes((item as any).uses));
62
62
  }
@@ -73,19 +73,11 @@ export function findLazyIncludes<TEnv = any>(
73
73
  export function evaluateLazyEntry<TEnv = any>(
74
74
  entry: RouteEntry<TEnv>,
75
75
  deps: LazyEvalDeps<TEnv>,
76
- ): void {
76
+ ): void | Promise<void> {
77
77
  if (!entry.lazy || entry.lazyEvaluated || !entry.lazyPatterns) {
78
78
  return;
79
79
  }
80
80
 
81
- // Check for pre-computed routes from build-time data.
82
- // Only leaf nodes (no nested includes) are precomputed, so entries with
83
- // nested lazy includes fall through to the handler below.
84
- // When multiple entries share the same staticPrefix (e.g., several
85
- // include("/", ...) calls), the precomputed data merges all their routes
86
- // into one entry. Assigning that merged set to the first matching entry
87
- // causes findMatch to pick the wrong handler for routes belonging to a
88
- // different include. Skip the shortcut when the prefix is shared.
89
81
  const currentPrecomputed = deps.getPrecomputedByPrefix();
90
82
  if (currentPrecomputed) {
91
83
  const routes = currentPrecomputed.get(entry.staticPrefix);
@@ -99,39 +91,76 @@ export function evaluateLazyEntry<TEnv = any>(
99
91
  for (const [name, pattern] of Object.entries(routes)) {
100
92
  deps.mergedRouteMap[name] = pattern;
101
93
  }
102
- registerRouteMap(deps.mergedRouteMap);
94
+ // Register only this entry's routes (the delta): the full
95
+ // mergedRouteMap is seeded from the generated manifest at
96
+ // createRouter() time and already registered there — re-passing it
97
+ // made this request-path call O(total routes) (issue #666).
98
+ registerRouteMap(routes);
103
99
  return;
104
100
  }
105
101
  }
106
102
  }
107
103
 
108
- // Mark as evaluated immediately to prevent concurrent evaluation.
109
- // JS is single-threaded but handlers.handler() could theoretically yield,
110
- // and the while-loop in findMatch retries after evaluation.
111
- entry.lazyEvaluated = true;
104
+ // Async provider (`() => import("./routes")`): the route module is evaluated
105
+ // off the startup path, on the first request reaching this prefix. Concurrent
106
+ // first-hits share one in-flight promise so the import + expansion run exactly
107
+ // once. The eager path below stays fully synchronous (no Promise), so the
108
+ // per-entry match loop pays no microtask for normal includes.
109
+ const lazyPatterns = entry.lazyPatterns;
110
+ if (isIncludeProvider(lazyPatterns)) {
111
+ const inflight = (entry as { _lazyInflight?: Promise<void> })._lazyInflight;
112
+ if (inflight) return inflight;
113
+ const work = (async () => {
114
+ const resolved = resolveIncludeModule(
115
+ await lazyPatterns(),
116
+ entry.staticPrefix,
117
+ );
118
+ // Cache the resolved patterns: any later re-entry expands synchronously.
119
+ entry.lazyPatterns = resolved as unknown as UrlPatterns<TEnv>;
120
+ runExpansion(entry, deps, resolved as UrlPatterns<TEnv>);
121
+ })();
122
+ (entry as { _lazyInflight?: Promise<void> })._lazyInflight = work;
123
+ const clear = () => {
124
+ (entry as { _lazyInflight?: Promise<void> })._lazyInflight = undefined;
125
+ };
126
+ // On failure, clear the flag (lazyEvaluated stays false) so a later request
127
+ // can retry the import rather than wedging the route permanently.
128
+ work.then(clear, clear);
129
+ return work;
130
+ }
131
+
132
+ runExpansion(entry, deps, lazyPatterns as UrlPatterns<TEnv>);
133
+ }
112
134
 
113
- const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
135
+ /**
136
+ * Synchronously expand a lazy entry's (already-resolved) patterns into routes
137
+ * and splice any nested lazy includes as new entries. Runs once per entry.
138
+ */
139
+ function runExpansion<TEnv = any>(
140
+ entry: RouteEntry<TEnv>,
141
+ deps: LazyEvalDeps<TEnv>,
142
+ lazyPatterns: UrlPatterns<TEnv>,
143
+ ): void {
144
+ // lazyEvaluated is set at the END, only after the handler ran and the routes
145
+ // (and any nested includes) were spliced. Setting it up-front would mark the
146
+ // entry done even if the handler throws mid-expansion: the async provider
147
+ // path clears _lazyInflight on rejection so a later request can retry, but a
148
+ // premature lazyEvaluated=true would make evaluateLazyEntry short-circuit
149
+ // (line ~77) forever, wedging the route at 404 until the isolate restarts.
114
150
  const lazyContext = entry.lazyContext;
115
151
 
116
- // Create a new context for evaluating the lazy patterns
117
152
  const manifest = new Map<string, EntryData>();
118
153
  const patterns = new Map<string, string>();
119
154
  const patternsByPrefix = new Map<string, Map<string, string>>();
120
155
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
121
156
 
122
- // Capture the handler result to detect nested lazy includes
123
157
  let handlerResult: AllUseItems[] = [];
124
158
 
125
- // Merge captured counters from include() to maintain consistent
126
- // shortCode indices with sibling entries from pattern extraction
127
- const lazyCounters: Record<string, number> = {};
128
- if (lazyContext?.counters) {
129
- for (const [key, value] of Object.entries(lazyContext.counters)) {
130
- lazyCounters[key] = value;
131
- }
132
- }
159
+ const lazyCounters: Record<string, number> = lazyContext?.counters
160
+ ? { ...lazyContext.counters }
161
+ : {};
133
162
 
134
- RSCRouterContext.run(
163
+ RangoContext.run(
135
164
  {
136
165
  manifest,
137
166
  patterns,
@@ -145,10 +174,8 @@ export function evaluateLazyEntry<TEnv = any>(
145
174
  includeScope: lazyContext?.includeScope,
146
175
  },
147
176
  () => {
148
- // Run the lazy patterns handler with the original context prefixes
149
- // The prefix comes from the IncludeItem stored in lazyPatterns
150
177
  const includePrefix = (entry as any)._lazyPrefix || "";
151
- const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
178
+ const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
152
179
 
153
180
  if (fullPrefix || lazyContext?.namePrefix) {
154
181
  runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
@@ -160,11 +187,9 @@ export function evaluateLazyEntry<TEnv = any>(
160
187
  },
161
188
  );
162
189
 
163
- // Populate the entry's routes from the patterns
164
190
  const routesObject: Record<string, string> = {};
165
191
  for (const [name, pattern] of patterns.entries()) {
166
192
  routesObject[name] = pattern;
167
- // Also add to merged route map for reverse() support
168
193
  const existingPattern = deps.mergedRouteMap[name];
169
194
  if (existingPattern !== undefined && existingPattern !== pattern) {
170
195
  console.warn(
@@ -175,46 +200,38 @@ export function evaluateLazyEntry<TEnv = any>(
175
200
  deps.mergedRouteMap[name] = pattern;
176
201
  }
177
202
 
178
- // Update the entry in-place
179
203
  entry.routes = routesObject as ResolvedRouteMap<any>;
180
204
 
181
- // Note: Do NOT clear lazyPatterns/lazyContext here.
182
- // loadManifest() needs them on every request to re-run the handler
183
- // in the correct AsyncLocalStorage context (Store.manifest).
184
-
185
- // Update trailing slash config if available
186
205
  if (trailingSlashMap.size > 0) {
187
206
  entry.trailingSlash = Object.fromEntries(trailingSlashMap);
188
207
  }
189
208
 
190
- // Detect nested lazy includes and register them as new entries
191
209
  const nestedLazyIncludes = findLazyIncludes(handlerResult);
192
210
  for (const lazyInclude of nestedLazyIncludes) {
193
- // Compute the full URL prefix (combining parent prefix if any)
194
- const fullPrefix = lazyInclude.context.urlPrefix
195
- ? lazyInclude.context.urlPrefix + lazyInclude.prefix
196
- : lazyInclude.prefix;
211
+ const fullPrefix = joinPrefix(
212
+ lazyInclude.context.urlPrefix,
213
+ lazyInclude.prefix,
214
+ );
197
215
 
198
216
  const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
199
217
  prefix: "",
200
218
  staticPrefix: extractStaticPrefix(fullPrefix),
201
- routes: {} as ResolvedRouteMap<any>, // Empty until first match
219
+ routes: {} as ResolvedRouteMap<any>,
202
220
  trailingSlash: entry.trailingSlash,
203
- handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
221
+ // include entries don't invoke their own handler (real handlers come from
222
+ // the expanded routes); use the parent's placeholder. A provider has no
223
+ // `.handler` until resolved, so never read it here.
224
+ handler: isIncludeProvider(lazyInclude.patterns)
225
+ ? entry.handler
226
+ : (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
204
227
  mountIndex: deps.nextMountIndex(),
205
228
  routerId: deps.routerId,
206
- // Lazy evaluation fields
207
229
  lazy: true,
208
230
  lazyPatterns: lazyInclude.patterns,
209
231
  lazyContext: lazyInclude.context,
210
232
  lazyEvaluated: false,
211
- // Store the include prefix for evaluation
212
233
  _lazyPrefix: lazyInclude.prefix,
213
234
  };
214
- // Insert nested lazy entry before any entry whose staticPrefix is a
215
- // prefix of (but shorter than) this lazy entry's staticPrefix.
216
- // This ensures more specific lazy includes are matched before
217
- // less specific eager entries (e.g., "/href/nested" before "/href/:id").
218
235
  const nestedPrefix = nestedEntry.staticPrefix;
219
236
  let insertIndex = deps.routesEntries.length;
220
237
  if (nestedPrefix) {
@@ -232,6 +249,12 @@ export function evaluateLazyEntry<TEnv = any>(
232
249
  deps.routesEntries.splice(insertIndex, 0, nestedEntry);
233
250
  }
234
251
 
235
- // Re-register route map for runtime reverse() usage
236
- registerRouteMap(deps.mergedRouteMap);
252
+ // Delta only see the matching comment on the precomputed branch above and
253
+ // the WHY block on registerRouteMap (issue #666).
254
+ registerRouteMap(routesObject);
255
+
256
+ // Expansion fully succeeded (handler ran, routes + nested includes spliced) —
257
+ // mark done now so a mid-expansion throw above leaves lazyEvaluated=false and
258
+ // the entry retriable.
259
+ entry.lazyEvaluated = true;
237
260
  }
@@ -5,8 +5,8 @@
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
10
  import { contextGet } from "../context-var.js";
11
11
  import type {
12
12
  ResolvedSegment,
@@ -19,12 +19,17 @@ import type {
19
19
  ErrorBoundaryFallbackProps,
20
20
  ErrorInfo,
21
21
  } from "../types";
22
- import type { LoaderRevalidationResult, ActionContext } from "./types";
23
22
  import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
24
  import { buildHandleSnapshot } from "../server/handle-store.js";
25
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
26
  import { _getRequestContext } from "../server/request-context.js";
27
- import { isInsideLoaderScope } from "../server/context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
32
+ } from "../server/context.js";
28
33
  import { debugLog } from "./logging.js";
29
34
 
30
35
  /**
@@ -66,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
66
71
  ) => ErrorInfo,
67
72
  onError?: LoaderErrorCallback,
68
73
  ): Promise<LoaderDataResult<T>> {
69
- // 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.
70
77
  const loaderName = segmentId.split(".").pop() || "unknown";
71
78
 
72
79
  return Promise.resolve(promise)
@@ -102,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
102
109
  };
103
110
  }
104
111
 
105
- // 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.
106
120
  let renderedFallback: ReactNode;
107
- if (typeof fallback === "function") {
108
- // ErrorBoundaryHandler - call with error info
109
- 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,
110
143
  error: errorInfo,
144
+ fallback: null,
111
145
  };
112
- renderedFallback = fallback(props);
113
- } else {
114
- renderedFallback = fallback;
115
146
  }
116
147
 
117
148
  debugLog("loader", "loader error wrapped with boundary fallback", {
@@ -266,7 +297,10 @@ function createLoaderExecutor<TEnv>(
266
297
  search: (ctx as any).search,
267
298
  pathname: ctx.pathname,
268
299
  url: ctx.url,
300
+ originalUrl: ctx.originalUrl,
269
301
  env: ctx.env,
302
+ waitUntil: ctx.waitUntil.bind(ctx),
303
+ executionContext: ctx.executionContext,
270
304
  get: ((keyOrVar: any) =>
271
305
  contextGet(variables, keyOrVar)) as typeof ctx.get,
272
306
  use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -284,6 +318,12 @@ function createLoaderExecutor<TEnv>(
284
318
  );
285
319
  }
286
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.
287
327
  const snapshot =
288
328
  reqCtx._renderBarrierHandleSnapshot ??
289
329
  buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
@@ -305,15 +345,7 @@ function createLoaderExecutor<TEnv>(
305
345
  );
306
346
  }
307
347
 
308
- // Guard: reject streaming trees
309
348
  const reqCtx = reqCtxRef ?? _getRequestContext();
310
- if (reqCtx?._treeHasStreaming) {
311
- throw new Error(
312
- `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
313
- `Streaming handlers may not have settled when rendered() resolves. ` +
314
- `Remove loading() from the route tree or restructure to avoid rendered().`,
315
- );
316
- }
317
349
 
318
350
  if (renderedPromise) return renderedPromise;
319
351
 
@@ -324,7 +356,10 @@ function createLoaderExecutor<TEnv>(
324
356
  }
325
357
 
326
358
  // Bidirectional deadlock check: if a handler already started
327
- // awaiting this loader, calling rendered() would deadlock.
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.
328
363
  if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
329
364
  throw new Error(
330
365
  `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
@@ -342,20 +377,70 @@ function createLoaderExecutor<TEnv>(
342
377
  }
343
378
  reqCtx._renderBarrierWaiters.add(currentLoaderId);
344
379
 
345
- renderedPromise = reqCtx._renderBarrier.then(() => {
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
+ }
346
403
  renderedResolved = true;
347
404
  });
348
405
  return renderedPromise;
349
406
  },
350
407
  };
351
408
 
352
- const doneLoader = track(`loader:${loader.$$id}`, 2);
353
- const promise = Promise.resolve(
354
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
355
- ).finally(() => {
356
- pendingLoaders.delete(loader.$$id);
357
- doneLoader();
358
- });
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
+ //
424
+ // `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
425
+ // LANE RULE: a handler-consumed loader's value is a BAKED copy in every
426
+ // shared artifact (cache(), "use cache", the PPR shell), so its identity
427
+ // reads are exempt from the shell-capture guard — same allowance the
428
+ // cache-purity guards give it. DSL segment loaders keep their lane
429
+ // machinery (live = masked at capture, bake = guarded). A DSL loader's
430
+ // nested deps inherit isDslLoader=false only when the CHAIN started in a
431
+ // handler; a chain started by the segment funnel stays DSL (the loader
432
+ // scope ALS survives the body's awaits).
433
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
434
+ Promise.resolve(
435
+ runInsideLoaderBodyScope(
436
+ () => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
437
+ loader.$$id,
438
+ !isDslLoader,
439
+ ),
440
+ ).finally(() => {
441
+ pendingLoaders.delete(loader.$$id);
442
+ }),
443
+ );
359
444
 
360
445
  loaderPromises.set(loader.$$id, promise);
361
446
  return promise;
@@ -387,12 +472,6 @@ export function setupLoaderAccess<TEnv>(
387
472
 
388
473
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
389
474
 
390
- // Track whether we're inside a handle push callback. Loaders started
391
- // from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
392
- // block segment resolution, so they must not be registered as handler
393
- // dependencies for deadlock detection.
394
- let insideHandlePush = false;
395
-
396
475
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
397
476
  if (isHandle(item)) {
398
477
  const handle = item;
@@ -407,35 +486,40 @@ export function setupLoaderAccess<TEnv>(
407
486
  );
408
487
  }
409
488
 
410
- return (
411
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
412
- ) => {
413
- if (!store) return;
414
-
415
- if (typeof dataOrFn === "function") {
416
- // Mark scope so ctx.use(loader) calls inside the callback
417
- // are not registered as handler-to-loader deps.
418
- insideHandlePush = true;
419
- try {
420
- const result = (dataOrFn as () => Promise<unknown>)();
489
+ return withDefer(
490
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
491
+ if (!store) return;
492
+
493
+ if (typeof dataOrFn === "function") {
494
+ // Run the callback inside the push-callback scope so ctx.use(loader)
495
+ // calls it makes including after its own awaits, for an async
496
+ // callback — are not registered as handler-to-loader deps and do not
497
+ // trip the deadlock guard. A pushed promise value is not tracked by
498
+ // handleStore.settled and does not block segment resolution, so it
499
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
500
+ // boolean) is what survives the callback's awaits.
501
+ const result = runInsidePushCallbackScope(() =>
502
+ (dataOrFn as () => Promise<unknown>)(),
503
+ );
421
504
  store.push(handle.$$id, segmentId, result);
422
- } finally {
423
- insideHandlePush = false;
505
+ return;
424
506
  }
425
- return;
426
- }
427
507
 
428
- store.push(handle.$$id, segmentId, dataOrFn);
429
- };
508
+ store.push(handle.$$id, segmentId, dataOrFn);
509
+ },
510
+ );
430
511
  }
431
512
 
432
513
  // Deadlock guard and handler-to-loader dependency tracking.
433
514
  // Skip when inside a DSL loader scope (resolveLoaderData also calls
434
515
  // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
435
516
  // inside a handle push callback (push callbacks don't block segment
436
- // resolution so they can't cause rendered() deadlocks).
517
+ // resolution so they can't cause rendered() deadlocks). The push-callback
518
+ // check is an ALS scope so it also exempts an ASYNC callback's continuation
519
+ // after its first await — relevant on streaming trees, where the guard
520
+ // state now stays live until handleStore.settled.
437
521
  const loader = item as LoaderDefinition<any, any>;
438
- if (!isInsideLoaderScope() && !insideHandlePush) {
522
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
439
523
  const reqCtx = reqCtxRef ?? _getRequestContext();
440
524
  if (reqCtx) {
441
525
  // Direction 1: handler awaits loader that already called rendered()
@@ -449,13 +533,18 @@ export function setupLoaderAccess<TEnv>(
449
533
  `Move the data dependency to a loader-to-loader pattern instead.`,
450
534
  );
451
535
  }
452
- // Direction 2: track dep so rendered() can detect the deadlock
453
- // if the loader calls it later. Skip when the barrier has already
454
- // resolved no deadlock is possible (rendered() resolves immediately).
455
- // _renderBarrierSegmentOrder is undefined before resolution, string[]
456
- // after. This also prevents false positives from handle push callbacks
457
- // that resume after their first await (post-barrier-resolution).
458
- if (reqCtx._renderBarrierSegmentOrder === undefined) {
536
+ // Direction 2: track dep so rendered() can detect the deadlock if the
537
+ // loader calls it later. Skip once the guard window is CLOSED — for a
538
+ // non-streaming tree that is when the barrier resolves (rendered()
539
+ // resolves immediately), and for a streaming tree it is when
540
+ // handleStore.settled completes (rendered() keeps waiting until then, so
541
+ // a loading() handler resuming after the barrier can still form a
542
+ // cycle). Using the explicit guard-closed flag rather than
543
+ // _renderBarrierSegmentOrder keeps tracking live across the streaming
544
+ // settle wait. (Handle push callbacks are already excluded above via
545
+ // isInsidePushCallbackScope(), so they cannot produce false positives
546
+ // here.)
547
+ if (!reqCtx._renderBarrierGuardClosed) {
459
548
  if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
460
549
  reqCtx._handlerLoaderDeps.add(loader.$$id);
461
550
  }
@@ -489,18 +578,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
489
578
  );
490
579
  }
491
580
 
492
- return (
493
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
494
- ) => {
495
- if (!store) return;
581
+ // Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
582
+ // prerender path, matching production setupLoaderAccess. Without it a
583
+ // prerender handler calling .defer() throws "defer is not a function".
584
+ return withDefer(
585
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
586
+ if (!store) return;
496
587
 
497
- const valueOrPromise =
498
- typeof dataOrFn === "function"
499
- ? (dataOrFn as () => Promise<unknown>)()
500
- : dataOrFn;
588
+ const valueOrPromise =
589
+ typeof dataOrFn === "function"
590
+ ? (dataOrFn as () => Promise<unknown>)()
591
+ : dataOrFn;
501
592
 
502
- store.push(handle.$$id, segmentId, valueOrPromise);
503
- };
593
+ store.push(handle.$$id, segmentId, valueOrPromise);
594
+ },
595
+ );
504
596
  }
505
597
 
506
598
  // Loader case: not available during pre-rendering
@@ -528,8 +620,11 @@ export function setupLoaderAccessSilent<TEnv>(
528
620
 
529
621
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
530
622
  if (isHandle(item)) {
531
- // Silent mode - return a no-op so handle data is not pushed during caching
532
- return (_dataOrFn: unknown) => {};
623
+ // Silent mode - return a no-op so handle data is not pushed during caching.
624
+ // Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
625
+ // callable resolver (also a no-op here), matching production's push shape
626
+ // instead of throwing "defer is not a function".
627
+ return withDefer((_dataOrFn: unknown) => {});
533
628
  }
534
629
 
535
630
  return useLoader(item as LoaderDefinition<any, any>, null);
@@ -1,8 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
 
4
- // -- Revalidation trace types --
5
-
6
4
  export interface RevalidationTraceEntry {
7
5
  segmentId: string;
8
6
  segmentType: string;
@@ -36,8 +34,6 @@ export interface RevalidationTrace {
36
34
  entries: RevalidationTraceEntry[];
37
35
  }
38
36
 
39
- // -- Log context --
40
-
41
37
  interface RouterLogContext {
42
38
  requestId: string;
43
39
  transactionId: string;
@@ -195,8 +191,6 @@ export function debugWarn(
195
191
  console.warn(`${prefix} ${message}`);
196
192
  }
197
193
 
198
- // -- Revalidation trace helpers --
199
-
200
194
  export function isTraceActive(): boolean {
201
195
  if (!INTERNAL_RANGO_DEBUG) return false;
202
196
  const ctx = routerLogContext.getStore();