@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
@@ -11,7 +11,15 @@
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";
16
24
  import type { ScopedReverseFunction } from "../reverse.js";
17
25
  import type {
@@ -33,11 +41,20 @@ import {
33
41
  type HandleData,
34
42
  } from "./handle-store.js";
35
43
  import { isHandle } from "../handle.js";
36
- import { track, type MetricsStore } from "./context.js";
44
+ import { withDefer } from "../defer.js";
45
+ import { type MetricsStore } from "./context.js";
46
+ import { observePhase, PHASES } from "../router/instrument.js";
37
47
  import { getFetchableLoader } from "./fetchable-loader-store.js";
38
48
  import type { SegmentCacheStore } from "../cache/types.js";
39
49
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
40
- 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";
41
58
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
42
59
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
43
60
  import { isInsideCacheScope } from "./context.js";
@@ -45,7 +62,12 @@ import {
45
62
  createReverseFunction,
46
63
  stripInternalParams,
47
64
  } from "../router/handler-context.js";
48
- import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
65
+ import {
66
+ getGlobalRouteMap,
67
+ isRouteRootScoped,
68
+ getSearchSchema,
69
+ } from "../route-map-builder.js";
70
+ import { parseSearchParams } from "../search-params.js";
49
71
  import { invariant } from "../errors.js";
50
72
  import { isAutoGeneratedRouteName } from "../route-name.js";
51
73
 
@@ -58,22 +80,7 @@ import { isAutoGeneratedRouteName } from "../route-name.js";
58
80
  export interface RequestContext<
59
81
  TEnv = DefaultEnv,
60
82
  TParams = Record<string, string>,
61
- > {
62
- /** Platform bindings (Cloudflare env, etc.) */
63
- env: TEnv;
64
- /** Original HTTP request */
65
- request: Request;
66
- /** Parsed URL (with internal `_rsc*` params stripped) */
67
- url: URL;
68
- /**
69
- * The original request URL with all parameters intact, including
70
- * internal `_rsc*` transport params.
71
- */
72
- originalUrl: URL;
73
- /** URL pathname */
74
- pathname: string;
75
- /** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
76
- searchParams: URLSearchParams;
83
+ > extends RequestScope<TEnv> {
77
84
  /** @internal Shared variable backing store for ctx.get()/ctx.set(). */
78
85
  _variables: Record<string, any>;
79
86
  /** Get a variable set by middleware */
@@ -115,6 +122,10 @@ export interface RequestContext<
115
122
  setStatus(status: number): void;
116
123
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
117
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;
118
129
 
119
130
  /**
120
131
  * Access loader data or push handle data.
@@ -150,29 +161,100 @@ export interface RequestContext<
150
161
  /** @internal Handle store for tracking handle data across segments */
151
162
  _handleStore: HandleStore;
152
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
+
153
173
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
154
174
  _cacheStore?: SegmentCacheStore;
155
175
 
176
+ /**
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 Bake-lane loader containers collected DURING a shell capture:
192
+ * segment-key -> the loader's (pre-wrap) result promise. Populated by
193
+ * resolveLoaderData for loaders on entries with no renderable loading() (the
194
+ * bake lane — they execute at capture instead of being masked; see
195
+ * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
196
+ * after the shell quiesces: settled containers are promise-elided,
197
+ * Flight-serialized, and pinned into the snapshot's loader family; a
198
+ * REJECTED container refuses the capture (error UI must never bake into the
199
+ * shared shell). Own property of the capture's derived context only.
200
+ */
201
+ _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
202
+
203
+ /**
204
+ * @internal Loader-family snapshot seed for a shell HIT's tail render:
205
+ * segment-key -> the capture's elided container (already Flight-deserialized
206
+ * by serveShellHit). resolveLoaderData overlays it onto the fresh run's
207
+ * container (recorded paths pinned, hole-marker paths keep the fresh nested
208
+ * promises) so the payload's baked bytes match the frozen prelude. Own
209
+ * property of the HIT tail's derived context only.
210
+ */
211
+ _shellLoaderSeed?: Map<string, unknown>;
212
+
213
+ /**
214
+ * @internal Set (to the offending fn name) by the cookies()/headers()
215
+ * capture guard when it throws DURING a capture render. Load-bearing for the
216
+ * bake lane: a guard throw inside an executing loader is swallowed by
217
+ * wrapLoaderPromise into per-loader error UI, which would otherwise bake
218
+ * silently into the shared shell — the capture checks this flag after the
219
+ * render and refuses instead. Deterministic, so the capture does not retry.
220
+ */
221
+ _shellCaptureGuardTripped?: string;
222
+
223
+ /**
224
+ * @internal The loader $$id whose BODY was executing when the capture guard
225
+ * tripped (read off the loader-body ALS scope at trip time), or undefined
226
+ * when the read came from handler/render code. Only used to make the
227
+ * once-per-key refusal warning name the real source — the old text
228
+ * hardcoded "a bake-lane loader", which misattributed handler-land reads
229
+ * and sent users debugging the wrong lane (issue #672, secondary).
230
+ */
231
+ _shellCaptureGuardTrippedLoaderId?: string;
232
+
233
+ /**
234
+ * @internal Handler-owned registry of explicit per-scope stores from
235
+ * cache({ store }). Created once per createRSCHandler() and threaded into
236
+ * every request context, so it accumulates every explicit store the handler
237
+ * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
238
+ * reach every store that may hold tagged entries. The app-level store is not
239
+ * added here (it is always reachable via _cacheStore).
240
+ */
241
+ _explicitTaggedStores?: Set<SegmentCacheStore>;
242
+
243
+ /**
244
+ * @internal Union of every cache tag resolved while producing this request's
245
+ * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
246
+ * Populated at the tag-resolution sites via recordRequestTags(). Read by the
247
+ * document cache middleware so a full-page entry is tagged with everything its
248
+ * content used and can therefore be invalidated by updateTag()/revalidateTag().
249
+ */
250
+ _requestTags: Set<string>;
251
+
156
252
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
157
253
  _cacheProfiles?: Record<
158
254
  string,
159
255
  import("../cache/profile-registry.js").CacheProfile
160
256
  >;
161
257
 
162
- /**
163
- * Schedule work to run after the response is sent.
164
- * On Cloudflare Workers, uses ctx.waitUntil().
165
- * On Node.js, runs as fire-and-forget.
166
- *
167
- * @example
168
- * ```typescript
169
- * ctx.waitUntil(async () => {
170
- * await cacheStore.set(key, data, ttl);
171
- * });
172
- * ```
173
- */
174
- waitUntil(fn: () => Promise<void>): void;
175
-
176
258
  /**
177
259
  * Register a callback to run when the response is created.
178
260
  * Callbacks are sync and receive the response. They can:
@@ -195,6 +277,19 @@ export interface RequestContext<
195
277
  /** @internal Registered onResponse callbacks */
196
278
  _onResponseCallbacks: Array<(response: Response) => Response>;
197
279
 
280
+ /**
281
+ * @internal Promises of the background tasks scheduled via this context's
282
+ * waitUntil (deferred cache writes, revalidations, consumer tasks). The PPR
283
+ * shell capture drains this list BEFORE its match/render as an ORDERING EDGE:
284
+ * every foreground deferred cache write is scheduled here before the capture
285
+ * task is, so settling the list first guarantees the capture's cache reads
286
+ * observe the foreground's generation instead of racing it (see
287
+ * shell-capture.ts). Tasks whose scheduling fn carries
288
+ * UNTRACKED_BACKGROUND_TASK are not tracked (the capture task itself —
289
+ * tracking it would make that drain await its own promise).
290
+ */
291
+ _pendingBackgroundTasks?: Array<Promise<unknown>>;
292
+
198
293
  /**
199
294
  * Current theme setting (only available when theme is enabled in router config)
200
295
  *
@@ -276,6 +371,30 @@ export interface RequestContext<
276
371
  /** @internal Previous route key (from the navigation source), used for revalidation */
277
372
  _prevRouteKey?: string;
278
373
 
374
+ /**
375
+ * @internal Navigation/action source data the transition({ when }) gate reads
376
+ * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
377
+ * come from the navigation snapshot (set at match time); action* are stashed
378
+ * at the action-bearing gate call sites. All undefined when there is no source
379
+ * (initial full load) or no action (plain navigation).
380
+ */
381
+ _gateCurrentUrl?: URL;
382
+ _gateCurrentParams?: Record<string, string>;
383
+ _gateActionId?: string;
384
+ _gateActionUrl?: URL;
385
+ _gateActionResult?: unknown;
386
+ _gateFormData?: FormData;
387
+
388
+ /**
389
+ * @internal True while the post-action revalidation render is running (set by
390
+ * revalidateAfterAction). The "use cache" runtime reads this to prefer
391
+ * freshness over a fast stale response during an action: a stale entry
392
+ * re-executes in the foreground (so the action response reflects the refreshed
393
+ * value) with only the store write deferred, instead of serving stale and
394
+ * revalidating in the background. A plain navigation (flag unset) keeps SWR.
395
+ */
396
+ _inActionRevalidation?: boolean;
397
+
279
398
  /**
280
399
  * @internal Render barrier for experimental `rendered()` API.
281
400
  * Resolves when all non-loader segments have settled and handle data
@@ -300,7 +419,9 @@ export interface RequestContext<
300
419
 
301
420
  /**
302
421
  * @internal Set to true when the matched entry tree contains any `loading()`
303
- * entries (streaming). Used by rendered() to fail fast.
422
+ * entries (streaming). On a streaming tree rendered() waits for the streaming
423
+ * handlers to settle (via handleStore.settled) before resolving, and the
424
+ * deadlock guard state is kept live until that wait completes.
304
425
  */
305
426
  _treeHasStreaming?: boolean;
306
427
 
@@ -324,6 +445,18 @@ export interface RequestContext<
324
445
  */
325
446
  _renderBarrierHandleSnapshot?: HandleData;
326
447
 
448
+ /**
449
+ * @internal The deadlock guard window is closed (no further handler-awaits-
450
+ * loader cycle is possible). For non-streaming trees this is set when the
451
+ * barrier resolves. For streaming trees the window stays open until
452
+ * handleStore.settled — rendered() keeps waiting past the barrier and a
453
+ * loading() handler can still resume and await a still-waiting loader — so it
454
+ * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
455
+ * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
456
+ * during the streaming settle wait.
457
+ */
458
+ _renderBarrierGuardClosed?: boolean;
459
+
327
460
  /** @internal Per-request error dedup set for onError reporting */
328
461
  _reportedErrors: WeakSet<object>;
329
462
 
@@ -331,9 +464,13 @@ export interface RequestContext<
331
464
  * @internal Report a non-fatal background error through the router's
332
465
  * onError callback. Wired by the RSC handler / router during request
333
466
  * creation. Cache-runtime and other subsystems call this to surface
334
- * errors without failing the response.
467
+ * errors without failing the response. `category` is surfaced to consumers as
468
+ * `metadata.category` on the onError context (phase `cache`).
335
469
  */
336
- _reportBackgroundError?: (error: unknown, category: string) => void;
470
+ _reportBackgroundError?: (
471
+ error: unknown,
472
+ category: CacheErrorCategory,
473
+ ) => void;
337
474
 
338
475
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
339
476
  _debugPerformance?: boolean;
@@ -341,6 +478,18 @@ export interface RequestContext<
341
478
  /** @internal Request-scoped performance metrics store */
342
479
  _metricsStore?: MetricsStore;
343
480
 
481
+ /**
482
+ * @internal True request entry timestamp (performance.now() at handler entry).
483
+ * Set once at request-context creation (rsc/handler.ts) so a metrics store
484
+ * created MID-request — ctx.debugPerformance() or the getMetricsStore wrapper —
485
+ * anchors its timeline to the real request start instead of the opt-in moment,
486
+ * keeping phases that began before the opt-in at their true (non-negative) offset.
487
+ */
488
+ _handlerStart?: number;
489
+
490
+ /** @internal Resolved platform phase-span tracing for this request (Cloudflare or OTel) */
491
+ _tracing?: ResolvedTracing;
492
+
344
493
  /** @internal Router basename for this request (used by redirect()) */
345
494
  _basename?: string;
346
495
 
@@ -349,6 +498,15 @@ export interface RequestContext<
349
498
  * to avoid a second resolveRoute call. Cleared on HMR invalidation.
350
499
  */
351
500
  _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
501
+
502
+ /**
503
+ * @internal Coarse route-level cache signal for the X-Rango-Cache debug
504
+ * header. Populated by match/matchPartial only when the debug cache signal
505
+ * gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
506
+ * the response-finalization path (createResponseWithMergedHeaders). Undefined
507
+ * when the gate is off, so no header is emitted.
508
+ */
509
+ _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
352
510
  }
353
511
 
354
512
  /**
@@ -367,13 +525,25 @@ export type PublicRequestContext<
367
525
  | "setCookie"
368
526
  | "deleteCookie"
369
527
  | "_handleStore"
528
+ | "_transitionWhen"
370
529
  | "_cacheStore"
530
+ | "_shellCaptureRun"
531
+ | "_shellCaptureGuardTrippedLoaderId"
532
+ | "_explicitTaggedStores"
533
+ | "_requestTags"
371
534
  | "_cacheProfiles"
372
535
  | "_onResponseCallbacks"
373
536
  | "_themeConfig"
374
537
  | "_locationState"
375
538
  | "_routeName"
376
539
  | "_prevRouteKey"
540
+ | "_gateCurrentUrl"
541
+ | "_gateCurrentParams"
542
+ | "_gateActionId"
543
+ | "_gateActionUrl"
544
+ | "_gateActionResult"
545
+ | "_gateFormData"
546
+ | "_inActionRevalidation"
377
547
  | "_reportedErrors"
378
548
  | "_renderBarrier"
379
549
  | "_resolveRenderBarrier"
@@ -382,16 +552,32 @@ export type PublicRequestContext<
382
552
  | "_renderBarrierWaiters"
383
553
  | "_handlerLoaderDeps"
384
554
  | "_renderBarrierHandleSnapshot"
555
+ | "_renderBarrierGuardClosed"
385
556
  | "_reportBackgroundError"
386
557
  | "_debugPerformance"
387
558
  | "_metricsStore"
559
+ | "_handlerStart"
388
560
  | "_basename"
389
561
  | "_setStatus"
562
+ | "_rotateStateCookie"
563
+ | "_setKeepCacheDirective"
390
564
  | "_variables"
391
565
  | "_classifiedRoute"
566
+ | "_cacheSignal"
392
567
  | "res"
393
568
  >;
394
569
 
570
+ /**
571
+ * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
572
+ * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
573
+ * the capture's pre-render write barrier settles that list, so tracking the
574
+ * capture itself would make the drain wait on its own (still-running) promise.
575
+ * @internal
576
+ */
577
+ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
578
+ "rango.untrackedBackgroundTask",
579
+ );
580
+
395
581
  // AsyncLocalStorage instance for request context
396
582
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
397
583
 
@@ -440,6 +626,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
440
626
  export function setRequestContextParams(
441
627
  params: Record<string, string>,
442
628
  routeName?: string,
629
+ routeMap?: Record<string, string>,
443
630
  ): void {
444
631
  const ctx = requestContextStorage.getStore();
445
632
  if (ctx) {
@@ -452,9 +639,13 @@ export function setRequestContextParams(
452
639
  : undefined
453
640
  ) as DefaultRouteName | undefined;
454
641
  }
455
- // Update reverse with scoped resolution now that route is known
642
+ // Update reverse with scoped resolution now that route is known. Production
643
+ // omits routeMap and uses the global map (routes are registered globally);
644
+ // the testing primitives (renderToFlightString/renderServerTree) pass a
645
+ // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
646
+ // registered last.
456
647
  ctx.reverse = createReverseFunction(
457
- getGlobalRouteMap(),
648
+ routeMap ?? getGlobalRouteMap(),
458
649
  routeName,
459
650
  params,
460
651
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -470,11 +661,17 @@ export function setRequestContextParams(
470
661
  */
471
662
  export function setRequestContextPrevRouteKey(
472
663
  prevRouteKey: string | undefined,
664
+ currentUrl?: URL,
665
+ currentParams?: Record<string, string>,
473
666
  ): void {
474
667
  const ctx = requestContextStorage.getStore();
475
- if (ctx && prevRouteKey !== undefined) {
476
- ctx._prevRouteKey = prevRouteKey;
477
- }
668
+ if (!ctx) return;
669
+ if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
670
+ // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
671
+ // effectiveFromMatch.params from the navigation snapshot). Same write point as
672
+ // _prevRouteKey, which doubles as fromRouteName.
673
+ if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
674
+ if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
478
675
  }
479
676
 
480
677
  /**
@@ -488,23 +685,7 @@ export function getLocationState(): LocationStateEntry[] | undefined {
488
685
  return ctx?._locationState;
489
686
  }
490
687
 
491
- /**
492
- * Get the current request context, throwing if not available
493
- * @deprecated Use getRequestContext() directly — it now throws if outside context
494
- */
495
- export function requireRequestContext<
496
- TEnv = DefaultEnv,
497
- >(): RequestContext<TEnv> {
498
- return getRequestContext<TEnv>();
499
- }
500
-
501
- /**
502
- * Cloudflare Workers ExecutionContext (subset we need)
503
- */
504
- export interface ExecutionContext {
505
- waitUntil(promise: Promise<any>): void;
506
- passThroughOnException(): void;
507
- }
688
+ export type { ExecutionContext };
508
689
 
509
690
  /**
510
691
  * Options for creating a request context
@@ -518,6 +699,11 @@ export interface CreateRequestContextOptions<TEnv> {
518
699
  initialResponse?: Response;
519
700
  /** Optional cache store for segment caching (used by CacheScope) */
520
701
  cacheStore?: SegmentCacheStore;
702
+ /**
703
+ * Handler-owned registry of explicit per-scope stores for cross-store tag
704
+ * invalidation. Created once per handler, reused across requests.
705
+ */
706
+ explicitTaggedStores?: Set<SegmentCacheStore>;
521
707
  /** Optional cache profiles for "use cache" resolution (per-router) */
522
708
  cacheProfiles?: Record<
523
709
  string,
@@ -527,6 +713,10 @@ export interface CreateRequestContextOptions<TEnv> {
527
713
  executionContext?: ExecutionContext;
528
714
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
529
715
  themeConfig?: ResolvedThemeConfig | null;
716
+ /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
717
+ stateCookieName?: string;
718
+ /** Build version, used as the prefix of a server-rotated rango state value. */
719
+ version?: string;
530
720
  }
531
721
 
532
722
  /**
@@ -547,15 +737,17 @@ export function createRequestContext<TEnv>(
547
737
  variables,
548
738
  initialResponse,
549
739
  cacheStore,
740
+ explicitTaggedStores,
550
741
  cacheProfiles,
551
742
  executionContext,
552
743
  themeConfig,
744
+ stateCookieName,
745
+ version: stateVersion,
553
746
  } = options;
554
747
  const cookieHeader = request.headers.get("Cookie");
748
+ let rangoStateRotated = false;
555
749
  let parsedCookies: Record<string, string> | null = null;
556
750
 
557
- // Create stub response for collecting headers/cookies.
558
- // All cookie/header mutations go here; cookie reads derive from it.
559
751
  let stubResponse = initialResponse
560
752
  ? new Response(null, {
561
753
  status: initialResponse.status,
@@ -564,11 +756,9 @@ export function createRequestContext<TEnv>(
564
756
  })
565
757
  : new Response(null, { status: 200 });
566
758
 
567
- // Create handle store and loader memoization for this request
568
759
  const handleStore = createHandleStore();
569
760
  const loaderPromises = new Map<string, Promise<any>>();
570
761
 
571
- // Lazy parse cookies from the original Cookie header
572
762
  const getParsedCookies = (): Record<string, string> => {
573
763
  if (!parsedCookies) {
574
764
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -576,7 +766,6 @@ export function createRequestContext<TEnv>(
576
766
  return parsedCookies;
577
767
  };
578
768
 
579
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
580
769
  let responseCookieCache: Map<string, string | null> | null = null;
581
770
  const getResponseCookies = (): Map<string, string | null> => {
582
771
  if (!responseCookieCache) {
@@ -588,8 +777,6 @@ export function createRequestContext<TEnv>(
588
777
  responseCookieCache = null;
589
778
  };
590
779
 
591
- // Guard: throw if a response-level side effect is called inside a cache() scope.
592
- // Uses ALS to detect the scope (set during segment resolution).
593
780
  function assertNotInsideCacheScopeALS(methodName: string): void {
594
781
  if (isInsideCacheScope()) {
595
782
  throw new Error(
@@ -600,8 +787,7 @@ export function createRequestContext<TEnv>(
600
787
  }
601
788
  }
602
789
 
603
- // Effective cookie read: response stub Set-Cookie wins, then original header.
604
- // The stub IS the source of truth for same-request mutations.
790
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
605
791
  const effectiveCookie = (name: string): string | undefined => {
606
792
  const mutations = getResponseCookies();
607
793
  if (mutations.has(name)) {
@@ -611,14 +797,11 @@ export function createRequestContext<TEnv>(
611
797
  return getParsedCookies()[name];
612
798
  };
613
799
 
614
- // Theme helpers (only used when themeConfig is provided)
615
800
  const getTheme = (): Theme | undefined => {
616
801
  if (!themeConfig) return undefined;
617
802
 
618
- // Use overlay-aware read so setTheme() in the same request is reflected
619
803
  const stored = effectiveCookie(themeConfig.storageKey);
620
804
  if (stored) {
621
- // Validate stored value
622
805
  if (stored === "system" && themeConfig.enableSystem) {
623
806
  return "system";
624
807
  }
@@ -632,15 +815,15 @@ export function createRequestContext<TEnv>(
632
815
  const setTheme = (theme: Theme): void => {
633
816
  if (!themeConfig) return;
634
817
 
635
- // Validate theme value
636
- if (theme !== "system" && !themeConfig.themes.includes(theme)) {
637
- console.warn(
638
- `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
639
- );
818
+ // Shared guard (isValidTheme): reject any value not in the configured theme
819
+ // set, AND reject "system" when system detection is off — a cookie of
820
+ // theme=system with enableSystem:false would re-apply a bogus class="system"
821
+ // on the next SSR.
822
+ if (!isValidTheme(theme, themeConfig)) {
823
+ warnInvalidTheme(theme, themeConfig);
640
824
  return;
641
825
  }
642
826
 
643
- // Write to stub — effectiveCookie() will pick it up on next read
644
827
  stubResponse.headers.append(
645
828
  "Set-Cookie",
646
829
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -652,10 +835,8 @@ export function createRequestContext<TEnv>(
652
835
  invalidateResponseCookieCache();
653
836
  };
654
837
 
655
- // Strip internal _rsc* params so userland sees a clean URL.
656
838
  const cleanUrl = stripInternalParams(url);
657
839
 
658
- // Build the context object first (without use), then add use
659
840
  const ctx: RequestContext<TEnv> = {
660
841
  env,
661
842
  request,
@@ -741,6 +922,45 @@ export function createRequestContext<TEnv>(
741
922
  stubResponse.headers.set(name, value);
742
923
  },
743
924
 
925
+ // Rotate the rango state cookie for the responding client (the server seat
926
+ // of invalidateClientCache). Writes ONE Set-Cookie per request with the
927
+ // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
928
+ // serializer), not the URL-encoded form serializeCookieValue would produce.
929
+ // The timestamp is strictly greater than the client's current one (inbound
930
+ // X-Rango-State), so a same-millisecond server rotation still differs from
931
+ // the client value and the divergence observer fires.
932
+ _rotateStateCookie(): void {
933
+ if (rangoStateRotated) return;
934
+ rangoStateRotated = true;
935
+ if (!stateCookieName) return;
936
+ // The client's current value, for the monotonic guard: prefer the
937
+ // X-Rango-State header (router navigation/prefetch fetches send it), but
938
+ // fall back to the request's rango state cookie — action POSTs / plain
939
+ // app fetch()s carry no router header yet DO send the cookie. Without the
940
+ // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
941
+ // leaving the divergence observer silent. `|| null` so an empty header
942
+ // ('' from proxy normalization) falls through instead of short-circuiting.
943
+ // getRawCookieValue reads the cookie undecoded (the wire value
944
+ // decodeStateValue decodes exactly once) AND is the same parser the client
945
+ // mirror uses, so both seats read the same jar entry.
946
+ const prevRaw =
947
+ (request.headers.get("x-rango-state") || null) ??
948
+ getRawCookieValue(cookieHeader, stateCookieName);
949
+ const value = mintStateValue(stateVersion ?? "0", prevRaw);
950
+ stubResponse.headers.append(
951
+ "Set-Cookie",
952
+ serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
953
+ );
954
+ invalidateResponseCookieCache();
955
+ },
956
+
957
+ // Set the keepClientCache() directive header. The action bridge reads it on
958
+ // the response and suppresses its automatic invalidation. `.set` makes this
959
+ // idempotent (one header regardless of call count).
960
+ _setKeepCacheDirective(): void {
961
+ stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
962
+ },
963
+
744
964
  setStatus(status: number): void {
745
965
  assertNotInsideCacheExec(ctx, "setStatus");
746
966
  assertNotInsideCacheScopeALS("setStatus");
@@ -757,28 +977,49 @@ export function createRequestContext<TEnv>(
757
977
  });
758
978
  },
759
979
 
760
- // Placeholder - will be replaced below
761
980
  use: null as any,
762
981
 
763
982
  method: request.method,
764
983
 
765
984
  _handleStore: handleStore,
985
+ _transitionWhen: [],
766
986
  _cacheStore: cacheStore,
987
+ _explicitTaggedStores: explicitTaggedStores,
988
+ _requestTags: new Set<string>(),
767
989
  _cacheProfiles: cacheProfiles,
768
990
 
769
991
  waitUntil(fn: () => Promise<void>): void {
992
+ // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
993
+ // non-async callback becomes a rejected promise handed to the host's
994
+ // waitUntil (logged as a background failure), instead of escaping into
995
+ // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
996
+ const task = Promise.resolve().then(fn);
997
+ // Track the task promise so the PPR shell capture can settle the
998
+ // foreground's deferred cache writes before its own match/render (the
999
+ // ordering edge; see _pendingBackgroundTasks). The capture task itself
1000
+ // opts out via the marker — the drain must never await its own promise.
1001
+ if (
1002
+ !(fn as { [UNTRACKED_BACKGROUND_TASK]?: boolean })[
1003
+ UNTRACKED_BACKGROUND_TASK
1004
+ ]
1005
+ ) {
1006
+ ctx._pendingBackgroundTasks?.push(task);
1007
+ }
770
1008
  if (executionContext?.waitUntil) {
771
- // Cloudflare Workers: use native waitUntil
772
- executionContext.waitUntil(fn());
1009
+ executionContext.waitUntil(task);
773
1010
  } else {
774
- // Node.js / dev: fire-and-forget with error logging
775
- fn().catch((err) =>
1011
+ // Node/dev fallback: fire-and-forget with error logging (the same
1012
+ // policy fireAndForgetWaitUntil applies).
1013
+ task.catch((err) =>
776
1014
  console.error("[waitUntil] Background task failed:", err),
777
1015
  );
778
1016
  }
779
1017
  },
780
1018
 
1019
+ executionContext,
1020
+
781
1021
  _onResponseCallbacks: [],
1022
+ _pendingBackgroundTasks: [],
782
1023
 
783
1024
  onResponse(callback: (response: Response) => Response): void {
784
1025
  assertNotInsideCacheExec(ctx, "onResponse");
@@ -786,7 +1027,6 @@ export function createRequestContext<TEnv>(
786
1027
  this._onResponseCallbacks.push(callback);
787
1028
  },
788
1029
 
789
- // Theme properties (only set when themeConfig is provided)
790
1030
  get theme() {
791
1031
  return themeConfig ? getTheme() : undefined;
792
1032
  },
@@ -809,20 +1049,19 @@ export function createRequestContext<TEnv>(
809
1049
 
810
1050
  _reportedErrors: new WeakSet<object>(),
811
1051
  _metricsStore: undefined,
1052
+ _handlerStart: undefined,
812
1053
 
813
- // Render barrier: deferred promise resolved after non-loader segments settle.
814
- _renderBarrier: null as any, // set below
815
- _resolveRenderBarrier: null as any, // set below
1054
+ _renderBarrier: null as any,
1055
+ _resolveRenderBarrier: null as any,
816
1056
  _renderBarrierSegmentOrder: undefined,
817
1057
 
818
1058
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
819
1059
  };
820
1060
 
821
- // Lazy render barrier: only allocate the Promise when a loader actually
822
- // calls rendered(). Requests that don't use rendered() pay zero cost.
1061
+ // Lazy allocation: only create Promise when a loader calls rendered().
823
1062
  let barrierResolved = false;
824
1063
  let resolveBarrier: (() => void) | undefined;
825
- ctx._renderBarrier = null as any; // lazy — created on first access
1064
+ ctx._renderBarrier = null as any;
826
1065
  ctx._resolveRenderBarrier = (
827
1066
  segments: Array<{ type: string; id: string }>,
828
1067
  ) => {
@@ -832,21 +1071,26 @@ export function createRequestContext<TEnv>(
832
1071
  .filter((s) => s.type !== "loader")
833
1072
  .map((s) => s.id);
834
1073
  ctx._renderBarrierSegmentOrder = segOrder;
835
- // Build and cache handle snapshot so loader ctx.use(handle) calls
836
- // don't rebuild it on every invocation.
837
- ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
838
- handleStore,
839
- segOrder,
840
- );
841
- ctx._renderBarrierWaiters = undefined;
842
- ctx._handlerLoaderDeps = undefined;
1074
+
1075
+ const closeGuard = () => {
1076
+ ctx._renderBarrierWaiters = undefined;
1077
+ ctx._handlerLoaderDeps = undefined;
1078
+ ctx._renderBarrierGuardClosed = true;
1079
+ };
1080
+
1081
+ if (ctx._treeHasStreaming) {
1082
+ handleStore.settled.then(closeGuard);
1083
+ } else {
1084
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
1085
+ handleStore,
1086
+ segOrder,
1087
+ );
1088
+ closeGuard();
1089
+ }
843
1090
  if (resolveBarrier) resolveBarrier();
844
1091
  };
845
1092
  Object.defineProperty(ctx, "_renderBarrier", {
846
1093
  get() {
847
- // Barrier already resolved (cache/prerender hit) or first lazy access.
848
- // Either way, replace the getter with a concrete value to avoid
849
- // repeated Promise.resolve() allocations on subsequent reads.
850
1094
  const p = barrierResolved
851
1095
  ? Promise.resolve()
852
1096
  : new Promise<void>((resolve) => {
@@ -862,32 +1106,33 @@ export function createRequestContext<TEnv>(
862
1106
  configurable: true,
863
1107
  });
864
1108
 
865
- // Now create use() with access to ctx
866
1109
  ctx.use = createUseFunction({
867
1110
  handleStore,
868
1111
  loaderPromises,
869
1112
  getContext: () => ctx,
870
1113
  });
871
1114
 
872
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
873
1115
  (ctx as any)[NOCACHE_SYMBOL] = true;
874
1116
  return ctx;
875
1117
  }
876
1118
 
877
- /**
878
- * Parse Set-Cookie headers from a response into effective cookie state.
879
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
880
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
881
- * Max-Age=0 is treated as a delete.
882
- */
883
- const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
1119
+ // Capture the Max-Age value so it can be parsed numerically. A leading zero
1120
+ // (Max-Age=05) is a non-zero lifetime, not a deletion; only a value that parses
1121
+ // to <= 0 marks a cookie for deletion. Pattern-matching a leading "0" misread
1122
+ // zero-prefixed values like 05 / 010 as deletions.
1123
+ const MAX_AGE_RE = /;\s*Max-Age\s*=\s*(-?\d+)/i;
1124
+
1125
+ function isCookieDeletion(header: string): boolean {
1126
+ const m = MAX_AGE_RE.exec(header);
1127
+ if (!m) return false;
1128
+ return Number(m[1]) <= 0;
1129
+ }
884
1130
 
885
1131
  function parseResponseCookies(response: Response): Map<string, string | null> {
886
1132
  const result = new Map<string, string | null>();
887
1133
  const setCookies = response.headers.getSetCookie();
888
1134
 
889
1135
  for (const header of setCookies) {
890
- // First segment before ';' is the name=value pair
891
1136
  const semiIdx = header.indexOf(";");
892
1137
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
893
1138
  const eqIdx = pair.indexOf("=");
@@ -899,49 +1144,22 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
899
1144
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
900
1145
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
901
1146
  } catch {
902
- // Malformed encoding — skip this entry
903
1147
  continue;
904
1148
  }
905
1149
 
906
- // Max-Age=0 means the cookie is being deleted
907
- const isDeleted = MAX_AGE_ZERO_RE.test(header);
1150
+ const isDeleted = isCookieDeletion(header);
908
1151
  result.set(name, isDeleted ? null : value);
909
1152
  }
910
1153
 
911
1154
  return result;
912
1155
  }
913
1156
 
914
- /**
915
- * Parse cookies from Cookie header
916
- */
917
- function parseCookiesFromHeader(
918
- cookieHeader: string | null,
919
- ): Record<string, string> {
920
- if (!cookieHeader) return {};
921
-
922
- const cookies: Record<string, string> = {};
923
- const pairs = cookieHeader.split(";");
924
-
925
- for (const pair of pairs) {
926
- const [name, ...rest] = pair.trim().split("=");
927
- if (name) {
928
- const raw = rest.join("=");
929
- try {
930
- cookies[name] = decodeURIComponent(raw);
931
- } catch {
932
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
933
- cookies[name] = raw;
934
- }
935
- }
936
- }
937
-
938
- return cookies;
939
- }
1157
+ // Re-exported for unit tests and the existing import path. The implementation
1158
+ // lives in the dependency-free ./cookie-parse leaf so consumers (e.g. the host
1159
+ // dispatcher) can share it without pulling this module's request-context graph.
1160
+ export { parseCookiesFromHeader };
940
1161
 
941
- /**
942
- * Serialize a cookie for Set-Cookie header
943
- */
944
- function serializeCookieValue(
1162
+ export function serializeCookieValue(
945
1163
  name: string,
946
1164
  value: string,
947
1165
  options: CookieOptions = {},
@@ -968,20 +1186,12 @@ export interface CreateUseFunctionOptions<TEnv> {
968
1186
  getContext: () => RequestContext<TEnv>;
969
1187
  }
970
1188
 
971
- /**
972
- * Create the use() function for loader and handle composition.
973
- *
974
- * This is the unified implementation used by both RequestContext and HandlerContext.
975
- * - For loaders: executes and memoizes loader functions
976
- * - For handles: returns a push function to add handle data
977
- */
978
1189
  export function createUseFunction<TEnv>(
979
1190
  options: CreateUseFunctionOptions<TEnv>,
980
1191
  ): RequestContext["use"] {
981
1192
  const { handleStore, loaderPromises, getContext } = options;
982
1193
 
983
1194
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
984
- // Handle case: return a push function
985
1195
  if (isHandle(item)) {
986
1196
  const handle = item;
987
1197
  const ctx = getContext();
@@ -994,30 +1204,24 @@ export function createUseFunction<TEnv>(
994
1204
  );
995
1205
  }
996
1206
 
997
- // Return a push function bound to this handle and segment
998
- return (
999
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
1000
- ) => {
1001
- // If it's a function, call it immediately to get the promise
1002
- const valueOrPromise =
1003
- typeof dataOrFn === "function"
1004
- ? (dataOrFn as () => Promise<unknown>)()
1005
- : dataOrFn;
1006
-
1007
- // Push directly - promises will be serialized by RSC and streamed
1008
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
1009
- };
1207
+ return withDefer(
1208
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
1209
+ const valueOrPromise =
1210
+ typeof dataOrFn === "function"
1211
+ ? (dataOrFn as () => Promise<unknown>)()
1212
+ : dataOrFn;
1213
+
1214
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
1215
+ },
1216
+ );
1010
1217
  }
1011
1218
 
1012
- // Loader case
1013
1219
  const loader = item as LoaderDefinition<any, any>;
1014
1220
 
1015
- // Return cached promise if already started
1016
1221
  if (loaderPromises.has(loader.$$id)) {
1017
1222
  return loaderPromises.get(loader.$$id);
1018
1223
  }
1019
1224
 
1020
- // Get loader function - either from loader object or fetchable registry
1021
1225
  let loaderFn = loader.fn;
1022
1226
  if (!loaderFn) {
1023
1227
  const fetchable = getFetchableLoader(loader.$$id);
@@ -1034,21 +1238,34 @@ export function createUseFunction<TEnv>(
1034
1238
 
1035
1239
  const ctx = getContext();
1036
1240
 
1037
- // Create loader context with recursive use() support
1241
+ // Build the typed ctx.search the same way the render path
1242
+ // (createHandlerContext) and the fetchable-loader path (loader-fetch.ts) do:
1243
+ // parse the route's search schema over the cleaned searchParams. The base
1244
+ // RequestContext carries no `search` field, so reading `(ctx as any).search`
1245
+ // here always yielded {} — dropping typed search for action/dispatch loaders.
1246
+ const searchSchema = ctx._routeName
1247
+ ? getSearchSchema(ctx._routeName)
1248
+ : undefined;
1249
+ const loaderSearch = searchSchema
1250
+ ? parseSearchParams(ctx.searchParams, searchSchema)
1251
+ : {};
1252
+
1038
1253
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
1039
1254
  params: ctx.params,
1040
1255
  routeParams: (ctx.params ?? {}) as Record<string, string>,
1041
1256
  request: ctx.request,
1042
1257
  searchParams: ctx.searchParams,
1043
- search: (ctx as any).search ?? {},
1258
+ search: loaderSearch,
1044
1259
  pathname: ctx.pathname,
1045
1260
  url: ctx.url,
1261
+ originalUrl: ctx.originalUrl,
1046
1262
  env: ctx.env as any,
1263
+ waitUntil: ctx.waitUntil.bind(ctx),
1264
+ executionContext: ctx.executionContext,
1047
1265
  get: ctx.get as any,
1048
1266
  use: (<TDep, TDepParams = any>(
1049
1267
  dep: LoaderDefinition<TDep, TDepParams>,
1050
1268
  ): Promise<TDep> => {
1051
- // Recursive call - will start dep loader if not already started
1052
1269
  return ctx.use(dep);
1053
1270
  }) as LoaderContext["use"],
1054
1271
  method: "GET",
@@ -1067,12 +1284,14 @@ export function createUseFunction<TEnv>(
1067
1284
  },
1068
1285
  };
1069
1286
 
1070
- const doneLoader = track(`loader:${loader.$$id}`, 2);
1071
- const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
1072
- doneLoader();
1073
- });
1287
+ // Meter through the same unified phase API as the loader-resolution funnel
1288
+ // (observePhase), so a loader resolved via this base request-context ctx.use
1289
+ // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
1290
+ // drift between the two ctx.use implementations.
1291
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
1292
+ Promise.resolve(loaderFn(loaderCtx)),
1293
+ );
1074
1294
 
1075
- // Memoize for subsequent calls
1076
1295
  loaderPromises.set(loader.$$id, promise);
1077
1296
 
1078
1297
  return promise;