@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
@@ -11,16 +11,65 @@
11
11
  */
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { parseCookiesFromHeader } from "./cookie-parse.js";
15
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
14
16
  import type { CookieOptions } from "../router/middleware.js";
17
+ import {
18
+ KEEP_CACHE_HEADER,
19
+ getRawCookieValue,
20
+ mintStateValue,
21
+ serializeStateCookie,
22
+ } from "../browser/cookie-name.js";
15
23
  import type { LoaderDefinition, LoaderContext } from "../types.js";
24
+ import type { ScopedReverseFunction } from "../reverse.js";
25
+ import type {
26
+ DefaultEnv,
27
+ DefaultReverseRouteMap,
28
+ DefaultRouteName,
29
+ } from "../types/global-namespace.js";
16
30
  import type { Handle } from "../handle.js";
17
- import { createHandleStore, type HandleStore } from "./handle-store.js";
31
+ import {
32
+ type ContextVar,
33
+ contextGet,
34
+ contextSet,
35
+ isNonCacheable,
36
+ } from "../context-var.js";
37
+ import {
38
+ createHandleStore,
39
+ buildHandleSnapshot,
40
+ type HandleStore,
41
+ type HandleData,
42
+ } from "./handle-store.js";
18
43
  import { isHandle } from "../handle.js";
19
- import { track } from "./context.js";
44
+ import { withDefer } from "../defer.js";
45
+ import { type MetricsStore } from "./context.js";
46
+ import { observePhase, PHASES } from "../router/instrument.js";
20
47
  import { getFetchableLoader } from "./fetchable-loader-store.js";
21
48
  import type { SegmentCacheStore } from "../cache/types.js";
22
49
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
23
- import { THEME_COOKIE } from "../theme/constants.js";
50
+ import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
51
+ import type { TransitionWhenFn } from "../types/segments.js";
52
+ import type { ResolvedTracing } from "../router/tracing.js";
53
+ import {
54
+ THEME_COOKIE,
55
+ isValidTheme,
56
+ warnInvalidTheme,
57
+ } from "../theme/constants.js";
58
+ import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
59
+ import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
60
+ import { isInsideCacheScope } from "./context.js";
61
+ import {
62
+ createReverseFunction,
63
+ stripInternalParams,
64
+ } from "../router/handler-context.js";
65
+ import {
66
+ getGlobalRouteMap,
67
+ isRouteRootScoped,
68
+ getSearchSchema,
69
+ } from "../route-map-builder.js";
70
+ import { parseSearchParams } from "../search-params.js";
71
+ import { invariant } from "../errors.js";
72
+ import { isAutoGeneratedRouteName } from "../route-name.js";
24
73
 
25
74
  /**
26
75
  * Unified request context available via getRequestContext()
@@ -29,46 +78,54 @@ import { THEME_COOKIE } from "../theme/constants.js";
29
78
  * Use this when you need access to request data outside of route handlers.
30
79
  */
31
80
  export interface RequestContext<
32
- TEnv = unknown,
81
+ TEnv = DefaultEnv,
33
82
  TParams = Record<string, string>,
34
- > {
35
- /** Platform bindings (Cloudflare env, etc.) */
36
- env: TEnv;
37
- /** Original HTTP request */
38
- request: Request;
39
- /** Parsed URL (system params like _rsc* are NOT filtered here) */
40
- url: URL;
41
- /** URL pathname */
42
- pathname: string;
43
- /** URL search params (system params like _rsc* are NOT filtered here) */
44
- searchParams: URLSearchParams;
45
- /** Variables set by middleware (same as ctx.var) */
46
- var: Record<string, any>;
83
+ > extends RequestScope<TEnv> {
84
+ /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
85
+ _variables: Record<string, any>;
47
86
  /** Get a variable set by middleware */
48
- get: <K extends string>(key: K) => any;
87
+ get: {
88
+ <T>(contextVar: ContextVar<T>): T | undefined;
89
+ <K extends string>(key: K): any;
90
+ };
49
91
  /** Set a variable (shared with middleware and handlers) */
50
- set: <K extends string>(key: K, value: any) => void;
92
+ set: {
93
+ <T>(
94
+ contextVar: ContextVar<T>,
95
+ value: T,
96
+ options?: { cache?: boolean },
97
+ ): void;
98
+ <K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
99
+ };
51
100
  /**
52
101
  * Route params (populated after route matching)
53
102
  * Initially empty, then set to matched params
54
103
  */
55
104
  params: TParams;
56
- /**
57
- * Stub response for setting headers/cookies
58
- * Headers set here are merged into the final response
59
- */
60
- res: Response;
105
+ /** @internal Stub response for collecting headers/cookies. Use ctx.headers or ctx.header() instead. */
106
+ readonly res: Response;
61
107
 
62
- /** Get a cookie value from the request */
108
+ /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
63
109
  cookie(name: string): string | undefined;
64
- /** Get all cookies from the request */
110
+ /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
65
111
  cookies(): Record<string, string>;
66
- /** Set a cookie on the response */
112
+ /** @internal Set a cookie on the response. Use cookies().set() instead. */
67
113
  setCookie(name: string, value: string, options?: CookieOptions): void;
68
- /** Delete a cookie */
69
- deleteCookie(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
114
+ /** @internal Delete a cookie. Use cookies().delete() instead. */
115
+ deleteCookie(
116
+ name: string,
117
+ options?: Pick<CookieOptions, "domain" | "path">,
118
+ ): void;
70
119
  /** Set a response header */
71
120
  header(name: string, value: string): void;
121
+ /** Set the response status code */
122
+ setStatus(status: number): void;
123
+ /** @internal Set status bypassing cache-exec guard (for framework error handling) */
124
+ _setStatus(status: number): void;
125
+ /** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
126
+ _rotateStateCookie(): void;
127
+ /** @internal Set the keepClientCache() directive header on the response. */
128
+ _setKeepCacheDirective(): void;
72
129
 
73
130
  /**
74
131
  * Access loader data or push handle data.
@@ -90,10 +147,12 @@ export interface RequestContext<
90
147
  * ```
91
148
  */
92
149
  use: {
93
- <T, TLoaderParams = any>(loader: LoaderDefinition<T, TLoaderParams>): Promise<T>;
94
- <TData, TAccumulated = TData[]>(handle: Handle<TData, TAccumulated>): (
95
- data: TData | Promise<TData> | (() => Promise<TData>)
96
- ) => void;
150
+ <T, TLoaderParams = any>(
151
+ loader: LoaderDefinition<T, TLoaderParams>,
152
+ ): Promise<T>;
153
+ <TData, TAccumulated = TData[]>(
154
+ handle: Handle<TData, TAccumulated>,
155
+ ): (data: TData | Promise<TData> | (() => Promise<TData>)) => void;
97
156
  };
98
157
 
99
158
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
@@ -102,22 +161,56 @@ export interface RequestContext<
102
161
  /** @internal Handle store for tracking handle data across segments */
103
162
  _handleStore: HandleStore;
104
163
 
164
+ /**
165
+ * @internal transition({ when }) predicates for segments matched this request,
166
+ * keyed by segment id. Collected during resolution (the function is stripped
167
+ * from the serialized segment config), then evaluated post-handler in
168
+ * rsc-rendering — outside any cache scope — to drop the transition of any
169
+ * segment whose predicate returns false.
170
+ */
171
+ _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
172
+
105
173
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
106
174
  _cacheStore?: SegmentCacheStore;
107
175
 
108
176
  /**
109
- * Schedule work to run after the response is sent.
110
- * On Cloudflare Workers, uses ctx.waitUntil().
111
- * On Node.js, runs as fire-and-forget.
112
- *
113
- * @example
114
- * ```typescript
115
- * ctx.waitUntil(async () => {
116
- * await cacheStore.set(key, data, ttl);
117
- * });
118
- * ```
177
+ * @internal PPR shell-capture ACTIVE marker. True ONLY inside the background
178
+ * capture task's derived request context (built by shell-capture.ts). This is
179
+ * the switch every capture-specific behavior reads: loader masking
180
+ * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
181
+ * cookies()/headers() capture guard (cookie-store.ts
182
+ * assertNotInsideShellCapture). The foreground render never sets it, so the
183
+ * served response is byte-identical to axis 1. The capture descriptor itself
184
+ * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
185
+ * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
186
+ * locally and passes it to scheduleShellCapture directly.
187
+ */
188
+ _shellCaptureRun?: boolean;
189
+
190
+ /**
191
+ * @internal Handler-owned registry of explicit per-scope stores from
192
+ * cache({ store }). Created once per createRSCHandler() and threaded into
193
+ * every request context, so it accumulates every explicit store the handler
194
+ * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
195
+ * reach every store that may hold tagged entries. The app-level store is not
196
+ * added here (it is always reachable via _cacheStore).
119
197
  */
120
- waitUntil(fn: () => Promise<void>): void;
198
+ _explicitTaggedStores?: Set<SegmentCacheStore>;
199
+
200
+ /**
201
+ * @internal Union of every cache tag resolved while producing this request's
202
+ * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
203
+ * Populated at the tag-resolution sites via recordRequestTags(). Read by the
204
+ * document cache middleware so a full-page entry is tagged with everything its
205
+ * content used and can therefore be invalidated by updateTag()/revalidateTag().
206
+ */
207
+ _requestTags: Set<string>;
208
+
209
+ /** @internal Cache profiles for "use cache" profile resolution (per-router) */
210
+ _cacheProfiles?: Record<
211
+ string,
212
+ import("../cache/profile-registry.js").CacheProfile
213
+ >;
121
214
 
122
215
  /**
123
216
  * Register a callback to run when the response is created.
@@ -141,6 +234,19 @@ export interface RequestContext<
141
234
  /** @internal Registered onResponse callbacks */
142
235
  _onResponseCallbacks: Array<(response: Response) => Response>;
143
236
 
237
+ /**
238
+ * @internal Promises of the background tasks scheduled via this context's
239
+ * waitUntil (deferred cache writes, revalidations, consumer tasks). The PPR
240
+ * shell capture drains this list BEFORE its match/render as an ORDERING EDGE:
241
+ * every foreground deferred cache write is scheduled here before the capture
242
+ * task is, so settling the list first guarantees the capture's cache reads
243
+ * observe the foreground's generation instead of racing it (see
244
+ * shell-capture.ts). Tasks whose scheduling fn carries
245
+ * UNTRACKED_BACKGROUND_TASK are not tracked (the capture task itself —
246
+ * tracking it would make that drain await its own promise).
247
+ */
248
+ _pendingBackgroundTasks?: Array<Promise<unknown>>;
249
+
144
250
  /**
145
251
  * Current theme setting (only available when theme is enabled in router config)
146
252
  *
@@ -178,8 +284,246 @@ export interface RequestContext<
178
284
 
179
285
  /** @internal Theme configuration (null if theme not enabled) */
180
286
  _themeConfig?: ResolvedThemeConfig | null;
287
+
288
+ /**
289
+ * Attach location state entries to the current response.
290
+ *
291
+ * For partial (SPA) requests, the state is included in the RSC payload
292
+ * metadata and merged into history.pushState on the client. For redirect
293
+ * responses, the state travels through the redirect payload so the target
294
+ * page can read it via useLocationState.
295
+ *
296
+ * Multiple calls accumulate entries.
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * ctx.setLocationState(Flash({ text: "Item saved!" }));
301
+ * ```
302
+ */
303
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
304
+
305
+ /** @internal Accumulated location state entries */
306
+ _locationState?: LocationStateEntry[];
307
+
308
+ /**
309
+ * The matched route name, if the route has an explicit name.
310
+ * Undefined before route matching or for unnamed routes.
311
+ * Includes the namespace prefix from include() (e.g., "blog.post").
312
+ */
313
+ routeName?: DefaultRouteName;
314
+
315
+ /**
316
+ * Generate URLs from route names.
317
+ * Uses the global route map. After route matching, scoped (`.name`) resolution
318
+ * works within the matched include() scope.
319
+ */
320
+ reverse: ScopedReverseFunction<
321
+ Record<string, string>,
322
+ DefaultReverseRouteMap
323
+ >;
324
+
325
+ /** @internal Route name from route matching, used for scoped reverse resolution */
326
+ _routeName?: string;
327
+
328
+ /** @internal Previous route key (from the navigation source), used for revalidation */
329
+ _prevRouteKey?: string;
330
+
331
+ /**
332
+ * @internal Navigation/action source data the transition({ when }) gate reads
333
+ * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
334
+ * come from the navigation snapshot (set at match time); action* are stashed
335
+ * at the action-bearing gate call sites. All undefined when there is no source
336
+ * (initial full load) or no action (plain navigation).
337
+ */
338
+ _gateCurrentUrl?: URL;
339
+ _gateCurrentParams?: Record<string, string>;
340
+ _gateActionId?: string;
341
+ _gateActionUrl?: URL;
342
+ _gateActionResult?: unknown;
343
+ _gateFormData?: FormData;
344
+
345
+ /**
346
+ * @internal True while the post-action revalidation render is running (set by
347
+ * revalidateAfterAction). The "use cache" runtime reads this to prefer
348
+ * freshness over a fast stale response during an action: a stale entry
349
+ * re-executes in the foreground (so the action response reflects the refreshed
350
+ * value) with only the store write deferred, instead of serving stale and
351
+ * revalidating in the background. A plain navigation (flag unset) keeps SWR.
352
+ */
353
+ _inActionRevalidation?: boolean;
354
+
355
+ /**
356
+ * @internal Render barrier for experimental `rendered()` API.
357
+ * Resolves when all non-loader segments have settled and handle data
358
+ * is available. Used by DSL loaders that call `ctx.rendered()`.
359
+ */
360
+ _renderBarrier: Promise<void>;
361
+
362
+ /**
363
+ * @internal Resolve the render barrier. Accepts resolved segments, filters
364
+ * out loaders, and captures non-loader segment IDs as the handle ordering.
365
+ * Called after segment resolution (fresh) or handle replay (cache/prerender).
366
+ */
367
+ _resolveRenderBarrier: (
368
+ segments: Array<{ type: string; id: string }>,
369
+ ) => void;
370
+
371
+ /**
372
+ * @internal Segment order at barrier resolution time, used by loader
373
+ * ctx.use(handle) to collect handle data in correct order.
374
+ */
375
+ _renderBarrierSegmentOrder?: string[];
376
+
377
+ /**
378
+ * @internal Set to true when the matched entry tree contains any `loading()`
379
+ * entries (streaming). On a streaming tree rendered() waits for the streaming
380
+ * handlers to settle (via handleStore.settled) before resolving, and the
381
+ * deadlock guard state is kept live until that wait completes.
382
+ */
383
+ _treeHasStreaming?: boolean;
384
+
385
+ /**
386
+ * @internal Loader IDs that have called rendered() and are waiting for the
387
+ * barrier. Used to detect deadlocks when a handler tries to await the same
388
+ * loader via ctx.use(Loader).
389
+ */
390
+ _renderBarrierWaiters?: Set<string>;
391
+
392
+ /**
393
+ * @internal Loader IDs that handlers have started awaiting via ctx.use().
394
+ * Used for bidirectional deadlock detection: if a loader later calls
395
+ * rendered() and a handler already awaits it, we can detect the deadlock.
396
+ */
397
+ _handlerLoaderDeps?: Set<string>;
398
+
399
+ /**
400
+ * @internal Cached HandleData snapshot built at barrier resolution time.
401
+ * Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
402
+ */
403
+ _renderBarrierHandleSnapshot?: HandleData;
404
+
405
+ /**
406
+ * @internal The deadlock guard window is closed (no further handler-awaits-
407
+ * loader cycle is possible). For non-streaming trees this is set when the
408
+ * barrier resolves. For streaming trees the window stays open until
409
+ * handleStore.settled — rendered() keeps waiting past the barrier and a
410
+ * loading() handler can still resume and await a still-waiting loader — so it
411
+ * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
412
+ * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
413
+ * during the streaming settle wait.
414
+ */
415
+ _renderBarrierGuardClosed?: boolean;
416
+
417
+ /** @internal Per-request error dedup set for onError reporting */
418
+ _reportedErrors: WeakSet<object>;
419
+
420
+ /**
421
+ * @internal Report a non-fatal background error through the router's
422
+ * onError callback. Wired by the RSC handler / router during request
423
+ * creation. Cache-runtime and other subsystems call this to surface
424
+ * errors without failing the response. `category` is surfaced to consumers as
425
+ * `metadata.category` on the onError context (phase `cache`).
426
+ */
427
+ _reportBackgroundError?: (
428
+ error: unknown,
429
+ category: CacheErrorCategory,
430
+ ) => void;
431
+
432
+ /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
433
+ _debugPerformance?: boolean;
434
+
435
+ /** @internal Request-scoped performance metrics store */
436
+ _metricsStore?: MetricsStore;
437
+
438
+ /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
439
+ _tracing?: ResolvedTracing;
440
+
441
+ /** @internal Router basename for this request (used by redirect()) */
442
+ _basename?: string;
443
+
444
+ /**
445
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
446
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
447
+ */
448
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
449
+
450
+ /**
451
+ * @internal Coarse route-level cache signal for the X-Rango-Cache debug
452
+ * header. Populated by match/matchPartial only when the debug cache signal
453
+ * gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
454
+ * the response-finalization path (createResponseWithMergedHeaders). Undefined
455
+ * when the gate is off, so no header is emitted.
456
+ */
457
+ _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
181
458
  }
182
459
 
460
+ /**
461
+ * Public view of RequestContext, without internal methods and fields.
462
+ *
463
+ * This is the type exported to library consumers. Internal code should
464
+ * use the full RequestContext interface directly.
465
+ */
466
+ export type PublicRequestContext<
467
+ TEnv = DefaultEnv,
468
+ TParams = Record<string, string>,
469
+ > = Omit<
470
+ RequestContext<TEnv, TParams>,
471
+ | "cookie"
472
+ | "cookies"
473
+ | "setCookie"
474
+ | "deleteCookie"
475
+ | "_handleStore"
476
+ | "_transitionWhen"
477
+ | "_cacheStore"
478
+ | "_shellCaptureRun"
479
+ | "_explicitTaggedStores"
480
+ | "_requestTags"
481
+ | "_cacheProfiles"
482
+ | "_onResponseCallbacks"
483
+ | "_themeConfig"
484
+ | "_locationState"
485
+ | "_routeName"
486
+ | "_prevRouteKey"
487
+ | "_gateCurrentUrl"
488
+ | "_gateCurrentParams"
489
+ | "_gateActionId"
490
+ | "_gateActionUrl"
491
+ | "_gateActionResult"
492
+ | "_gateFormData"
493
+ | "_inActionRevalidation"
494
+ | "_reportedErrors"
495
+ | "_renderBarrier"
496
+ | "_resolveRenderBarrier"
497
+ | "_renderBarrierSegmentOrder"
498
+ | "_treeHasStreaming"
499
+ | "_renderBarrierWaiters"
500
+ | "_handlerLoaderDeps"
501
+ | "_renderBarrierHandleSnapshot"
502
+ | "_renderBarrierGuardClosed"
503
+ | "_reportBackgroundError"
504
+ | "_debugPerformance"
505
+ | "_metricsStore"
506
+ | "_basename"
507
+ | "_setStatus"
508
+ | "_rotateStateCookie"
509
+ | "_setKeepCacheDirective"
510
+ | "_variables"
511
+ | "_classifiedRoute"
512
+ | "_cacheSignal"
513
+ | "res"
514
+ >;
515
+
516
+ /**
517
+ * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
518
+ * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
519
+ * the capture's pre-render write barrier settles that list, so tracking the
520
+ * capture itself would make the drain wait on its own (still-running) promise.
521
+ * @internal
522
+ */
523
+ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
524
+ "rango.untrackedBackgroundTask",
525
+ );
526
+
183
527
  // AsyncLocalStorage instance for request context
184
528
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
185
529
 
@@ -189,16 +533,33 @@ const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
189
533
  */
190
534
  export function runWithRequestContext<TEnv, T>(
191
535
  context: RequestContext<TEnv>,
192
- fn: () => T
536
+ fn: () => T,
193
537
  ): T {
194
538
  return requestContextStorage.run(context, fn);
195
539
  }
196
540
 
197
541
  /**
198
542
  * Get the current request context
199
- * Returns undefined if not running within a request context
543
+ * Throws if called outside of a request context
544
+ */
545
+ export function getRequestContext<TEnv = DefaultEnv>(): RequestContext<TEnv> {
546
+ const ctx = requestContextStorage.getStore() as
547
+ | RequestContext<TEnv>
548
+ | undefined;
549
+ invariant(
550
+ ctx,
551
+ "getRequestContext() called outside of a request context. " +
552
+ "This function must be called from within a route handler, loader, middleware, " +
553
+ "server action, or server component.",
554
+ );
555
+ return ctx;
556
+ }
557
+
558
+ /**
559
+ * @internal Get the request context without throwing — for internal code that
560
+ * may run outside a request context (cache stores, optional handle lookups, etc.)
200
561
  */
201
- export function getRequestContext<TEnv = unknown>():
562
+ export function _getRequestContext<TEnv = DefaultEnv>():
202
563
  | RequestContext<TEnv>
203
564
  | undefined {
204
565
  return requestContextStorage.getStore() as RequestContext<TEnv> | undefined;
@@ -206,38 +567,72 @@ export function getRequestContext<TEnv = unknown>():
206
567
 
207
568
  /**
208
569
  * Update params on the current request context
209
- * Called after route matching to populate route params
570
+ * Called after route matching to populate route params and route name
210
571
  */
211
- export function setRequestContextParams(params: Record<string, string>): void {
572
+ export function setRequestContextParams(
573
+ params: Record<string, string>,
574
+ routeName?: string,
575
+ routeMap?: Record<string, string>,
576
+ ): void {
212
577
  const ctx = requestContextStorage.getStore();
213
578
  if (ctx) {
214
579
  ctx.params = params;
580
+ if (routeName !== undefined) {
581
+ ctx._routeName = routeName;
582
+ ctx.routeName = (
583
+ routeName && !isAutoGeneratedRouteName(routeName)
584
+ ? routeName
585
+ : undefined
586
+ ) as DefaultRouteName | undefined;
587
+ }
588
+ // Update reverse with scoped resolution now that route is known. Production
589
+ // omits routeMap and uses the global map (routes are registered globally);
590
+ // the testing primitives (renderToFlightString/renderServerTree) pass a
591
+ // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
592
+ // registered last.
593
+ ctx.reverse = createReverseFunction(
594
+ routeMap ?? getGlobalRouteMap(),
595
+ routeName,
596
+ params,
597
+ routeName ? isRouteRootScoped(routeName) : undefined,
598
+ );
215
599
  }
216
600
  }
217
601
 
218
602
  /**
219
- * Get the current request context, throwing if not available
220
- * Use this when context is required (e.g., in loader actions)
603
+ * Store the previous route key on the request context.
604
+ * Called during partial-match context creation to make the navigation source
605
+ * route key available for revalidation and intercept evaluation.
606
+ * @internal
221
607
  */
222
- export function requireRequestContext<TEnv = unknown>(): RequestContext<TEnv> {
223
- const ctx = getRequestContext<TEnv>();
224
- if (!ctx) {
225
- throw new Error(
226
- "Request context not available. This function must be called from within a server action " +
227
- "executed through the RSC handler."
228
- );
229
- }
230
- return ctx;
608
+ export function setRequestContextPrevRouteKey(
609
+ prevRouteKey: string | undefined,
610
+ currentUrl?: URL,
611
+ currentParams?: Record<string, string>,
612
+ ): void {
613
+ const ctx = requestContextStorage.getStore();
614
+ if (!ctx) return;
615
+ if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
616
+ // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
617
+ // effectiveFromMatch.params from the navigation snapshot). Same write point as
618
+ // _prevRouteKey, which doubles as fromRouteName.
619
+ if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
620
+ if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
231
621
  }
232
622
 
233
623
  /**
234
- * Cloudflare Workers ExecutionContext (subset we need)
624
+ * Get accumulated location state entries from the current request context.
625
+ * Returns undefined if no state has been set.
626
+ *
627
+ * @internal Used by the RSC handler to include state in payload metadata.
235
628
  */
236
- export interface ExecutionContext {
237
- waitUntil(promise: Promise<any>): void;
238
- passThroughOnException(): void;
629
+ export function getLocationState(): LocationStateEntry[] | undefined {
630
+ const ctx = getRequestContext();
631
+ return ctx?._locationState;
239
632
  }
240
633
 
634
+ export type { ExecutionContext };
635
+
241
636
  /**
242
637
  * Options for creating a request context
243
638
  */
@@ -246,12 +641,28 @@ export interface CreateRequestContextOptions<TEnv> {
246
641
  request: Request;
247
642
  url: URL;
248
643
  variables: Record<string, any>;
644
+ /** Optional initial response stub headers/status to seed effective cookie reads */
645
+ initialResponse?: Response;
249
646
  /** Optional cache store for segment caching (used by CacheScope) */
250
647
  cacheStore?: SegmentCacheStore;
648
+ /**
649
+ * Handler-owned registry of explicit per-scope stores for cross-store tag
650
+ * invalidation. Created once per handler, reused across requests.
651
+ */
652
+ explicitTaggedStores?: Set<SegmentCacheStore>;
653
+ /** Optional cache profiles for "use cache" resolution (per-router) */
654
+ cacheProfiles?: Record<
655
+ string,
656
+ import("../cache/profile-registry.js").CacheProfile
657
+ >;
251
658
  /** Optional Cloudflare execution context for waitUntil support */
252
659
  executionContext?: ExecutionContext;
253
660
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
254
661
  themeConfig?: ResolvedThemeConfig | null;
662
+ /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
663
+ stateCookieName?: string;
664
+ /** Build version, used as the prefix of a server-rotated rango state value. */
665
+ version?: string;
255
666
  }
256
667
 
257
668
  /**
@@ -263,20 +674,37 @@ export interface CreateRequestContextOptions<TEnv> {
263
674
  * - Passed to handlers as ctx
264
675
  */
265
676
  export function createRequestContext<TEnv>(
266
- options: CreateRequestContextOptions<TEnv>
677
+ options: CreateRequestContextOptions<TEnv>,
267
678
  ): RequestContext<TEnv> {
268
- const { env, request, url, variables, cacheStore, executionContext, themeConfig } = options;
679
+ const {
680
+ env,
681
+ request,
682
+ url,
683
+ variables,
684
+ initialResponse,
685
+ cacheStore,
686
+ explicitTaggedStores,
687
+ cacheProfiles,
688
+ executionContext,
689
+ themeConfig,
690
+ stateCookieName,
691
+ version: stateVersion,
692
+ } = options;
269
693
  const cookieHeader = request.headers.get("Cookie");
694
+ let rangoStateRotated = false;
270
695
  let parsedCookies: Record<string, string> | null = null;
271
696
 
272
- // Create stub response for collecting headers/cookies
273
- const stubResponse = new Response(null, { status: 200 });
697
+ let stubResponse = initialResponse
698
+ ? new Response(null, {
699
+ status: initialResponse.status,
700
+ statusText: initialResponse.statusText,
701
+ headers: new Headers(initialResponse.headers),
702
+ })
703
+ : new Response(null, { status: 200 });
274
704
 
275
- // Create handle store and loader memoization for this request
276
705
  const handleStore = createHandleStore();
277
706
  const loaderPromises = new Map<string, Promise<any>>();
278
707
 
279
- // Lazy parse cookies
280
708
  const getParsedCookies = (): Record<string, string> => {
281
709
  if (!parsedCookies) {
282
710
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -284,13 +712,42 @@ export function createRequestContext<TEnv>(
284
712
  return parsedCookies;
285
713
  };
286
714
 
287
- // Theme helpers (only used when themeConfig is provided)
715
+ let responseCookieCache: Map<string, string | null> | null = null;
716
+ const getResponseCookies = (): Map<string, string | null> => {
717
+ if (!responseCookieCache) {
718
+ responseCookieCache = parseResponseCookies(stubResponse);
719
+ }
720
+ return responseCookieCache;
721
+ };
722
+ const invalidateResponseCookieCache = () => {
723
+ responseCookieCache = null;
724
+ };
725
+
726
+ function assertNotInsideCacheScopeALS(methodName: string): void {
727
+ if (isInsideCacheScope()) {
728
+ throw new Error(
729
+ `ctx.${methodName}() cannot be called inside a cache() boundary. ` +
730
+ `On cache hit the handler is skipped, so this side effect would be lost. ` +
731
+ `Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
732
+ );
733
+ }
734
+ }
735
+
736
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
737
+ const effectiveCookie = (name: string): string | undefined => {
738
+ const mutations = getResponseCookies();
739
+ if (mutations.has(name)) {
740
+ const v = mutations.get(name);
741
+ return v === null ? undefined : v;
742
+ }
743
+ return getParsedCookies()[name];
744
+ };
745
+
288
746
  const getTheme = (): Theme | undefined => {
289
747
  if (!themeConfig) return undefined;
290
748
 
291
- const stored = getParsedCookies()[themeConfig.storageKey];
749
+ const stored = effectiveCookie(themeConfig.storageKey);
292
750
  if (stored) {
293
- // Validate stored value
294
751
  if (stored === "system" && themeConfig.enableSystem) {
295
752
  return "system";
296
753
  }
@@ -304,135 +761,353 @@ export function createRequestContext<TEnv>(
304
761
  const setTheme = (theme: Theme): void => {
305
762
  if (!themeConfig) return;
306
763
 
307
- // Validate theme value
308
- if (theme !== "system" && !themeConfig.themes.includes(theme)) {
309
- console.warn(`[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`);
764
+ // Shared guard (isValidTheme): reject any value not in the configured theme
765
+ // set, AND reject "system" when system detection is off — a cookie of
766
+ // theme=system with enableSystem:false would re-apply a bogus class="system"
767
+ // on the next SSR.
768
+ if (!isValidTheme(theme, themeConfig)) {
769
+ warnInvalidTheme(theme, themeConfig);
310
770
  return;
311
771
  }
312
772
 
313
- // Set cookie
314
773
  stubResponse.headers.append(
315
774
  "Set-Cookie",
316
775
  serializeCookieValue(themeConfig.storageKey, theme, {
317
776
  path: THEME_COOKIE.path,
318
777
  maxAge: THEME_COOKIE.maxAge,
319
778
  sameSite: THEME_COOKIE.sameSite,
320
- })
779
+ }),
321
780
  );
781
+ invalidateResponseCookieCache();
322
782
  };
323
783
 
324
- // Build the context object first (without use), then add use
784
+ const cleanUrl = stripInternalParams(url);
785
+
325
786
  const ctx: RequestContext<TEnv> = {
326
787
  env,
327
788
  request,
328
- url,
789
+ url: cleanUrl,
790
+ originalUrl: new URL(request.url),
329
791
  pathname: url.pathname,
330
- searchParams: url.searchParams,
331
- var: variables,
332
- get: <K extends string>(key: K) => variables[key],
333
- set: <K extends string>(key: K, value: any) => {
334
- variables[key] = value;
335
- },
792
+ searchParams: cleanUrl.searchParams,
793
+ _variables: variables,
794
+ get: ((keyOrVar: any) => {
795
+ if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
796
+ throw new Error(
797
+ `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
798
+ `The variable was created with { cache: false } or set with { cache: false }, ` +
799
+ `and its value would be stale on cache hit. Move the read outside the cached scope.`,
800
+ );
801
+ }
802
+ return contextGet(variables, keyOrVar);
803
+ }) as RequestContext<TEnv>["get"],
804
+ set: ((keyOrVar: any, value: any, options?: any) => {
805
+ assertNotInsideCacheExec(ctx, "set");
806
+ contextSet(variables, keyOrVar, value, options);
807
+ }) as RequestContext<TEnv>["set"],
336
808
  params: {} as Record<string, string>,
337
- res: stubResponse,
809
+
810
+ get res(): Response {
811
+ return stubResponse;
812
+ },
813
+ set res(_: Response) {
814
+ throw new Error(
815
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
816
+ );
817
+ },
338
818
 
339
819
  cookie(name: string): string | undefined {
340
- return getParsedCookies()[name];
820
+ return effectiveCookie(name);
341
821
  },
342
822
 
343
823
  cookies(): Record<string, string> {
344
- return { ...getParsedCookies() };
824
+ const parsed = getParsedCookies();
825
+ const mutations = getResponseCookies();
826
+ if (mutations.size === 0) return { ...parsed };
827
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
828
+ const deleted = new Set<string>();
829
+ for (const [k, v] of mutations) {
830
+ if (v === null) deleted.add(k);
831
+ }
832
+ const result: Record<string, string> = {};
833
+ for (const key of Object.keys(parsed)) {
834
+ if (!deleted.has(key)) result[key] = parsed[key];
835
+ }
836
+ for (const [k, v] of mutations) {
837
+ if (v !== null) result[k] = v;
838
+ }
839
+ return result;
345
840
  },
346
841
 
347
842
  setCookie(name: string, value: string, options?: CookieOptions): void {
843
+ assertNotInsideCacheExec(ctx, "setCookie");
844
+ assertNotInsideCacheScopeALS("setCookie");
348
845
  stubResponse.headers.append(
349
846
  "Set-Cookie",
350
- serializeCookieValue(name, value, options)
847
+ serializeCookieValue(name, value, options),
351
848
  );
849
+ invalidateResponseCookieCache();
352
850
  },
353
851
 
354
852
  deleteCookie(
355
853
  name: string,
356
- options?: Pick<CookieOptions, "domain" | "path">
854
+ options?: Pick<CookieOptions, "domain" | "path">,
357
855
  ): void {
856
+ assertNotInsideCacheExec(ctx, "deleteCookie");
857
+ assertNotInsideCacheScopeALS("deleteCookie");
358
858
  stubResponse.headers.append(
359
859
  "Set-Cookie",
360
- serializeCookieValue(name, "", { ...options, maxAge: 0 })
860
+ serializeCookieValue(name, "", { ...options, maxAge: 0 }),
361
861
  );
862
+ invalidateResponseCookieCache();
362
863
  },
363
864
 
364
865
  header(name: string, value: string): void {
866
+ assertNotInsideCacheExec(ctx, "header");
867
+ assertNotInsideCacheScopeALS("header");
365
868
  stubResponse.headers.set(name, value);
366
869
  },
367
870
 
368
- // Placeholder - will be replaced below
871
+ // Rotate the rango state cookie for the responding client (the server seat
872
+ // of invalidateClientCache). Writes ONE Set-Cookie per request with the
873
+ // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
874
+ // serializer), not the URL-encoded form serializeCookieValue would produce.
875
+ // The timestamp is strictly greater than the client's current one (inbound
876
+ // X-Rango-State), so a same-millisecond server rotation still differs from
877
+ // the client value and the divergence observer fires.
878
+ _rotateStateCookie(): void {
879
+ if (rangoStateRotated) return;
880
+ rangoStateRotated = true;
881
+ if (!stateCookieName) return;
882
+ // The client's current value, for the monotonic guard: prefer the
883
+ // X-Rango-State header (router navigation/prefetch fetches send it), but
884
+ // fall back to the request's rango state cookie — action POSTs / plain
885
+ // app fetch()s carry no router header yet DO send the cookie. Without the
886
+ // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
887
+ // leaving the divergence observer silent. `|| null` so an empty header
888
+ // ('' from proxy normalization) falls through instead of short-circuiting.
889
+ // getRawCookieValue reads the cookie undecoded (the wire value
890
+ // decodeStateValue decodes exactly once) AND is the same parser the client
891
+ // mirror uses, so both seats read the same jar entry.
892
+ const prevRaw =
893
+ (request.headers.get("x-rango-state") || null) ??
894
+ getRawCookieValue(cookieHeader, stateCookieName);
895
+ const value = mintStateValue(stateVersion ?? "0", prevRaw);
896
+ stubResponse.headers.append(
897
+ "Set-Cookie",
898
+ serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
899
+ );
900
+ invalidateResponseCookieCache();
901
+ },
902
+
903
+ // Set the keepClientCache() directive header. The action bridge reads it on
904
+ // the response and suppresses its automatic invalidation. `.set` makes this
905
+ // idempotent (one header regardless of call count).
906
+ _setKeepCacheDirective(): void {
907
+ stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
908
+ },
909
+
910
+ setStatus(status: number): void {
911
+ assertNotInsideCacheExec(ctx, "setStatus");
912
+ assertNotInsideCacheScopeALS("setStatus");
913
+ stubResponse = new Response(null, {
914
+ status,
915
+ headers: stubResponse.headers,
916
+ });
917
+ },
918
+
919
+ _setStatus(status: number): void {
920
+ stubResponse = new Response(null, {
921
+ status,
922
+ headers: stubResponse.headers,
923
+ });
924
+ },
925
+
369
926
  use: null as any,
370
927
 
371
928
  method: request.method,
372
929
 
373
930
  _handleStore: handleStore,
931
+ _transitionWhen: [],
374
932
  _cacheStore: cacheStore,
933
+ _explicitTaggedStores: explicitTaggedStores,
934
+ _requestTags: new Set<string>(),
935
+ _cacheProfiles: cacheProfiles,
375
936
 
376
937
  waitUntil(fn: () => Promise<void>): void {
938
+ // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
939
+ // non-async callback becomes a rejected promise handed to the host's
940
+ // waitUntil (logged as a background failure), instead of escaping into
941
+ // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
942
+ const task = Promise.resolve().then(fn);
943
+ // Track the task promise so the PPR shell capture can settle the
944
+ // foreground's deferred cache writes before its own match/render (the
945
+ // ordering edge; see _pendingBackgroundTasks). The capture task itself
946
+ // opts out via the marker — the drain must never await its own promise.
947
+ if (
948
+ !(fn as { [UNTRACKED_BACKGROUND_TASK]?: boolean })[
949
+ UNTRACKED_BACKGROUND_TASK
950
+ ]
951
+ ) {
952
+ ctx._pendingBackgroundTasks?.push(task);
953
+ }
377
954
  if (executionContext?.waitUntil) {
378
- // Cloudflare Workers: use native waitUntil
379
- executionContext.waitUntil(fn());
955
+ executionContext.waitUntil(task);
380
956
  } else {
381
- // Node.js / dev: fire-and-forget with error logging
382
- fn().catch((err) => console.error("[waitUntil] Background task failed:", err));
957
+ // Node/dev fallback: fire-and-forget with error logging (the same
958
+ // policy fireAndForgetWaitUntil applies).
959
+ task.catch((err) =>
960
+ console.error("[waitUntil] Background task failed:", err),
961
+ );
383
962
  }
384
963
  },
385
964
 
965
+ executionContext,
966
+
386
967
  _onResponseCallbacks: [],
968
+ _pendingBackgroundTasks: [],
387
969
 
388
970
  onResponse(callback: (response: Response) => Response): void {
971
+ assertNotInsideCacheExec(ctx, "onResponse");
972
+ assertNotInsideCacheScopeALS("onResponse");
389
973
  this._onResponseCallbacks.push(callback);
390
974
  },
391
975
 
392
- // Theme properties (only set when themeConfig is provided)
393
- theme: themeConfig ? getTheme() : undefined,
394
- setTheme: themeConfig ? setTheme : undefined,
976
+ get theme() {
977
+ return themeConfig ? getTheme() : undefined;
978
+ },
979
+ setTheme: themeConfig
980
+ ? (theme: Theme) => {
981
+ assertNotInsideCacheExec(ctx, "setTheme");
982
+ setTheme(theme);
983
+ }
984
+ : undefined,
395
985
  _themeConfig: themeConfig,
986
+
987
+ setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void {
988
+ assertNotInsideCacheExec(ctx, "setLocationState");
989
+ const arr = Array.isArray(entries) ? entries : [entries];
990
+ this._locationState = this._locationState
991
+ ? [...this._locationState, ...arr]
992
+ : arr;
993
+ },
994
+ _locationState: undefined,
995
+
996
+ _reportedErrors: new WeakSet<object>(),
997
+ _metricsStore: undefined,
998
+
999
+ _renderBarrier: null as any,
1000
+ _resolveRenderBarrier: null as any,
1001
+ _renderBarrierSegmentOrder: undefined,
1002
+
1003
+ reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
396
1004
  };
397
1005
 
398
- // Now create use() with access to ctx
1006
+ // Lazy allocation: only create Promise when a loader calls rendered().
1007
+ let barrierResolved = false;
1008
+ let resolveBarrier: (() => void) | undefined;
1009
+ ctx._renderBarrier = null as any;
1010
+ ctx._resolveRenderBarrier = (
1011
+ segments: Array<{ type: string; id: string }>,
1012
+ ) => {
1013
+ if (barrierResolved) return;
1014
+ barrierResolved = true;
1015
+ const segOrder = segments
1016
+ .filter((s) => s.type !== "loader")
1017
+ .map((s) => s.id);
1018
+ ctx._renderBarrierSegmentOrder = segOrder;
1019
+
1020
+ const closeGuard = () => {
1021
+ ctx._renderBarrierWaiters = undefined;
1022
+ ctx._handlerLoaderDeps = undefined;
1023
+ ctx._renderBarrierGuardClosed = true;
1024
+ };
1025
+
1026
+ if (ctx._treeHasStreaming) {
1027
+ handleStore.settled.then(closeGuard);
1028
+ } else {
1029
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
1030
+ handleStore,
1031
+ segOrder,
1032
+ );
1033
+ closeGuard();
1034
+ }
1035
+ if (resolveBarrier) resolveBarrier();
1036
+ };
1037
+ Object.defineProperty(ctx, "_renderBarrier", {
1038
+ get() {
1039
+ const p = barrierResolved
1040
+ ? Promise.resolve()
1041
+ : new Promise<void>((resolve) => {
1042
+ resolveBarrier = resolve;
1043
+ });
1044
+ Object.defineProperty(ctx, "_renderBarrier", {
1045
+ value: p,
1046
+ writable: false,
1047
+ configurable: false,
1048
+ });
1049
+ return p;
1050
+ },
1051
+ configurable: true,
1052
+ });
1053
+
399
1054
  ctx.use = createUseFunction({
400
1055
  handleStore,
401
1056
  loaderPromises,
402
1057
  getContext: () => ctx,
403
1058
  });
404
1059
 
1060
+ (ctx as any)[NOCACHE_SYMBOL] = true;
405
1061
  return ctx;
406
1062
  }
407
1063
 
408
- /**
409
- * Parse cookies from Cookie header
410
- */
411
- function parseCookiesFromHeader(
412
- cookieHeader: string | null
413
- ): Record<string, string> {
414
- if (!cookieHeader) return {};
415
-
416
- const cookies: Record<string, string> = {};
417
- const pairs = cookieHeader.split(";");
418
-
419
- for (const pair of pairs) {
420
- const [name, ...rest] = pair.trim().split("=");
421
- if (name) {
422
- cookies[name] = decodeURIComponent(rest.join("="));
1064
+ // Capture the Max-Age value so it can be parsed numerically. A leading zero
1065
+ // (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
1066
+ // to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
1067
+ // zero-prefixed values like 05 / 010 as deletions.
1068
+ const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;
1069
+
1070
+ function isCookieDeletion(header: string): boolean {
1071
+ const m = MAX_AGE_RE.exec(header);
1072
+ if (!m) return false;
1073
+ return Number(m[1]) <= 0;
1074
+ }
1075
+
1076
+ function parseResponseCookies(response: Response): Map<string, string | null> {
1077
+ const result = new Map<string, string | null>();
1078
+ const setCookies = response.headers.getSetCookie();
1079
+
1080
+ for (const header of setCookies) {
1081
+ const semiIdx = header.indexOf(";");
1082
+ const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
1083
+ const eqIdx = pair.indexOf("=");
1084
+ if (eqIdx === -1) continue;
1085
+
1086
+ let name: string;
1087
+ let value: string;
1088
+ try {
1089
+ name = decodeURIComponent(pair.substring(0, eqIdx).trim());
1090
+ value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
1091
+ } catch {
1092
+ continue;
423
1093
  }
1094
+
1095
+ const isDeleted = isCookieDeletion(header);
1096
+ result.set(name, isDeleted ? null : value);
424
1097
  }
425
1098
 
426
- return cookies;
1099
+ return result;
427
1100
  }
428
1101
 
429
- /**
430
- * Serialize a cookie for Set-Cookie header
431
- */
432
- function serializeCookieValue(
1102
+ // Re-exported for unit tests and the existing import path. The implementation
1103
+ // lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
1104
+ // dispatcher) can share it without pulling this module's request-context graph.
1105
+ export { parseCookiesFromHeader };
1106
+
1107
+ export function serializeCookieValue(
433
1108
  name: string,
434
1109
  value: string,
435
- options: CookieOptions = {}
1110
+ options: CookieOptions = {},
436
1111
  ): string {
437
1112
  let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
438
1113
 
@@ -456,20 +1131,12 @@ export interface CreateUseFunctionOptions<TEnv> {
456
1131
  getContext: () => RequestContext<TEnv>;
457
1132
  }
458
1133
 
459
- /**
460
- * Create the use() function for loader and handle composition.
461
- *
462
- * This is the unified implementation used by both RequestContext and HandlerContext.
463
- * - For loaders: executes and memoizes loader functions
464
- * - For handles: returns a push function to add handle data
465
- */
466
1134
  export function createUseFunction<TEnv>(
467
- options: CreateUseFunctionOptions<TEnv>
1135
+ options: CreateUseFunctionOptions<TEnv>,
468
1136
  ): RequestContext["use"] {
469
1137
  const { handleStore, loaderPromises, getContext } = options;
470
1138
 
471
1139
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
472
- // Handle case: return a push function
473
1140
  if (isHandle(item)) {
474
1141
  const handle = item;
475
1142
  const ctx = getContext();
@@ -478,31 +1145,28 @@ export function createUseFunction<TEnv>(
478
1145
  if (!segmentId) {
479
1146
  throw new Error(
480
1147
  `Handle "${handle.$$id}" used outside of handler context. ` +
481
- `Handles must be used within route/layout handlers.`
1148
+ `Handles must be used within route/layout handlers.`,
482
1149
  );
483
1150
  }
484
1151
 
485
- // Return a push function bound to this handle and segment
486
- return (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
487
- // If it's a function, call it immediately to get the promise
488
- const valueOrPromise = typeof dataOrFn === "function"
489
- ? (dataOrFn as () => Promise<unknown>)()
490
- : dataOrFn;
1152
+ return withDefer(
1153
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
1154
+ const valueOrPromise =
1155
+ typeof dataOrFn === "function"
1156
+ ? (dataOrFn as () => Promise<unknown>)()
1157
+ : dataOrFn;
491
1158
 
492
- // Push directly - promises will be serialized by RSC and streamed
493
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
494
- };
1159
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
1160
+ },
1161
+ );
495
1162
  }
496
1163
 
497
- // Loader case
498
1164
  const loader = item as LoaderDefinition<any, any>;
499
1165
 
500
- // Return cached promise if already started
501
1166
  if (loaderPromises.has(loader.$$id)) {
502
1167
  return loaderPromises.get(loader.$$id);
503
1168
  }
504
1169
 
505
- // Get loader function - either from loader object or fetchable registry
506
1170
  let loaderFn = loader.fn;
507
1171
  if (!loaderFn) {
508
1172
  const fetchable = getFetchableLoader(loader.$$id);
@@ -513,39 +1177,66 @@ export function createUseFunction<TEnv>(
513
1177
 
514
1178
  if (!loaderFn) {
515
1179
  throw new Error(
516
- `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.`
1180
+ `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.`,
517
1181
  );
518
1182
  }
519
1183
 
520
1184
  const ctx = getContext();
521
1185
 
522
- // Create loader context with recursive use() support
1186
+ // Build the typed ctx.search the same way the render path
1187
+ // (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
1188
+ // parse the route's search schema over the cleaned searchParams. The base
1189
+ // RequestContext carries no `search` field, so reading `(ctx as any).search`
1190
+ // here always yielded {} — dropping typed search for action/dispatch loaders.
1191
+ const searchSchema = ctx._routeName
1192
+ ? getSearchSchema(ctx._routeName)
1193
+ : undefined;
1194
+ const loaderSearch = searchSchema
1195
+ ? parseSearchParams(ctx.searchParams, searchSchema)
1196
+ : {};
1197
+
523
1198
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
524
1199
  params: ctx.params,
1200
+ routeParams: (ctx.params ?? {}) as Record<string, string>,
525
1201
  request: ctx.request,
526
1202
  searchParams: ctx.searchParams,
1203
+ search: loaderSearch,
527
1204
  pathname: ctx.pathname,
528
1205
  url: ctx.url,
1206
+ originalUrl: ctx.originalUrl,
529
1207
  env: ctx.env as any,
530
- var: ctx.var as any,
1208
+ waitUntil: ctx.waitUntil.bind(ctx),
1209
+ executionContext: ctx.executionContext,
531
1210
  get: ctx.get as any,
532
- use: <TDep, TDepParams = any>(
533
- dep: LoaderDefinition<TDep, TDepParams>
1211
+ use: (<TDep, TDepParams = any>(
1212
+ dep: LoaderDefinition<TDep, TDepParams>,
534
1213
  ): Promise<TDep> => {
535
- // Recursive call - will start dep loader if not already started
536
1214
  return ctx.use(dep);
537
- },
1215
+ }) as LoaderContext["use"],
538
1216
  method: "GET",
539
1217
  body: undefined,
1218
+ reverse: createReverseFunction(
1219
+ getGlobalRouteMap(),
1220
+ ctx._routeName,
1221
+ ctx.params as Record<string, string>,
1222
+ ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
1223
+ ),
1224
+ rendered: () => {
1225
+ throw new Error(
1226
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
1227
+ `It cannot be used from request-context loaders or server actions.`,
1228
+ );
1229
+ },
540
1230
  };
541
1231
 
542
- // Start loader execution with tracking
543
- const doneLoader = track(`loader:${loader.$$id}`);
544
- const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
545
- doneLoader();
546
- });
1232
+ // Meter through the same unified phase API as the loader-resolution funnel
1233
+ // (observePhase), so a loader resolved via this base request-context ctx.use
1234
+ // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
1235
+ // drift between the two ctx.use implementations.
1236
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
1237
+ Promise.resolve(loaderFn(loaderCtx)),
1238
+ );
547
1239
 
548
- // Memoize for subsequent calls
549
1240
  loaderPromises.set(loader.$$id, promise);
550
1241
 
551
1242
  return promise;