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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  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 +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -1,10 +1,13 @@
1
1
  /**
2
- * runLoader — unit-test a raw loader function in isolation.
2
+ * runLoader — unit-test a loader function in isolation.
3
3
  *
4
- * Consumers pass the RAW async loader body `(ctx) => ...`, NOT a createLoader()
5
- * handle. This sidesteps the Vite `$$id` injection that createLoader() relies on
6
- * for RSC registration: the function is invoked directly with a constructed
7
- * LoaderContext, so no build step is required.
4
+ * Pass the RAW async loader body `(ctx) => ...`, or a registered `createLoader()`
5
+ * handle (its fn is recovered from the fetchable registry by `$$id`). The raw
6
+ * body needs no build step; the handle works because `createLoader` assigns a
7
+ * runtime-fallback `$$id` and registers its fn even without the Vite plugin (when
8
+ * imported through the server build — the consumer's `@rangojs/router` under the
9
+ * `rangoTestConfig()` preset). Either way the function is invoked directly with a
10
+ * constructed LoaderContext.
8
11
  *
9
12
  * The LoaderContext mirrors the canonical shape the router builds at runtime
10
13
  * (see createUseFunction in server/request-context.ts). The loader runs inside
@@ -35,16 +38,20 @@ import {
35
38
  type RequestContext,
36
39
  } from "../server/request-context.js";
37
40
  import { createReverseFunction } from "../router/handler-context.js";
41
+ import { getFetchableLoader } from "../server/fetchable-loader-store.js";
38
42
  import type { LoaderContext, LoaderDefinition } from "../types.js";
39
43
  import type { ContextVar } from "../context-var.js";
40
44
  import { isHandle, type Handle } from "../handle.js";
45
+ import { collectHandle } from "./collect-handle.js";
41
46
  import type { ThemeConfig } from "../theme/types.js";
42
47
  import type { SegmentCacheStore } from "../cache/types.js";
43
48
  import type { CacheProfile } from "../cache/profile-registry.js";
44
49
  import {
45
50
  createTestRequestContext,
51
+ buildRunSnapshot,
46
52
  type CreateTestContextOptions,
47
53
  type VarsInit,
54
+ type StateCookieSeed,
48
55
  } from "./internal/context.js";
49
56
 
50
57
  /**
@@ -99,7 +106,9 @@ export interface RunLoaderOptions<TEnv = any> {
99
106
  basename?: string;
100
107
  /**
101
108
  * Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
102
- * or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert.
109
+ * or `{ themes: [...] }`). Seeds the request's theme config so nested handler
110
+ * or cache contexts created from this loader observe it. Loaders themselves do
111
+ * not expose `ctx.theme`/`ctx.setTheme` (those are handler/middleware-only).
103
112
  */
104
113
  theme?: ThemeConfig | true;
105
114
  /** Environment bindings surfaced as `ctx.env`. */
@@ -118,7 +127,15 @@ export interface RunLoaderOptions<TEnv = any> {
118
127
  body?: unknown;
119
128
  /** Form data surfaced as `ctx.formData`. */
120
129
  formData?: FormData;
121
- /** Resolver for `ctx.use(OtherLoader)` composition. */
130
+ /**
131
+ * Seed the data `ctx.use(OtherLoader)` returns, by loader REFERENCE — the same
132
+ * tuple form `renderHandler` / `renderRoute` use (`[[OtherLoader, data]]`).
133
+ * Matched by reference, so a real `createLoader()` handle resolves regardless
134
+ * of its build-injected `$$id`. For dynamic resolution (compute per dependency)
135
+ * use `use` instead; `loaders` is checked first.
136
+ */
137
+ loaders?: ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>;
138
+ /** Resolver for `ctx.use(OtherLoader)` composition (dynamic; `loaders` wins if both match). */
122
139
  use?: UseResolver;
123
140
  /**
124
141
  * Cache store backing `use cache` functions the loader invokes. Without it,
@@ -129,6 +146,13 @@ export interface RunLoaderOptions<TEnv = any> {
129
146
  cacheStore?: SegmentCacheStore;
130
147
  /** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
131
148
  cacheProfiles?: Record<string, CacheProfile>;
149
+ /**
150
+ * Customize the rango state cookie a loader that calls
151
+ * `invalidateClientCache()` rotates (the name is always seeded — default
152
+ * `rango-state_router_0` — so it rotates like production). Assert via the
153
+ * `Set-Cookie` on the request context's response.
154
+ */
155
+ stateCookie?: StateCookieSeed;
132
156
  /**
133
157
  * Mock the `ctx.rendered()` render barrier so a loader that does
134
158
  * `await ctx.rendered()` (to read handle data pushed during render) can be
@@ -155,10 +179,6 @@ export interface RunLoaderOptions<TEnv = any> {
155
179
  handles?: ReadonlyArray<readonly [Handle<any, any>, unknown]>;
156
180
  }
157
181
 
158
- /**
159
- * Merge `search` into a request's URL, returning a value `toRequest` can build.
160
- * Keeps the original method/headers/body when a Request was passed.
161
- */
162
182
  function withSearch(
163
183
  request: Request | string | undefined,
164
184
  search: Record<string, string> | undefined,
@@ -179,26 +199,37 @@ function withSearch(
179
199
  return url.toString();
180
200
  }
181
201
 
182
- /**
183
- * Run a raw loader body and return its resolved data.
184
- *
185
- * @example
186
- * ```ts
187
- * const data = await runLoader(
188
- * async (ctx) => ({ id: ctx.params.id, user: ctx.get("user") }),
189
- * { params: { id: "42" }, vars: { user: { name: "Ada" } } },
190
- * );
191
- * ```
192
- */
193
- export async function runLoader<T>(
194
- loaderFn: (ctx: TestLoaderContext) => Promise<T> | T,
195
- opts: RunLoaderOptions = {},
196
- ): Promise<T> {
197
- const ctxOpts: CreateTestContextOptions<any> = {
202
+ /** A raw loader body, or a registered `createLoader()` handle (its fn is recovered). */
203
+ export type RunnableLoader<T> =
204
+ | ((ctx: TestLoaderContext) => Promise<T> | T)
205
+ | LoaderDefinition<T, any>;
206
+
207
+ function resolveLoaderFn<T>(
208
+ loader: RunnableLoader<T>,
209
+ ): (ctx: TestLoaderContext) => Promise<T> | T {
210
+ if (typeof loader === "function") {
211
+ return loader as (ctx: TestLoaderContext) => Promise<T> | T;
212
+ }
213
+ const def = loader as LoaderDefinition<T, any>;
214
+ const fn = def.fn ?? getFetchableLoader(def.$$id)?.fn;
215
+ if (!fn) {
216
+ throw new Error(
217
+ `runLoader() received a createLoader() handle whose function could not be ` +
218
+ `recovered (id "${def.$$id || "<empty>"}"). The loader was likely imported ` +
219
+ `through the CLIENT build, which drops the body. Either import it through ` +
220
+ `@rangojs/router with the rangoTestConfig() preset (resolves to the server ` +
221
+ `build that registers the fn), or pass the raw loader body directly: ` +
222
+ `runLoader((ctx) => ...).`,
223
+ );
224
+ }
225
+ return fn as (ctx: TestLoaderContext) => Promise<T> | T;
226
+ }
227
+
228
+ function buildLoaderCtxOpts(
229
+ opts: RunLoaderOptions,
230
+ ): CreateTestContextOptions<any> {
231
+ return {
198
232
  env: opts.env,
199
- // Bake opts.search into the request URL itself so ctx.request.url, ctx.url,
200
- // and ctx.searchParams all agree (production carries the query string on the
201
- // real request — a loader reading ctx.request.url must see it too).
202
233
  request: withSearch(opts.request, opts.search),
203
234
  requestInit: opts.method ? { method: opts.method } : undefined,
204
235
  vars: opts.vars,
@@ -209,27 +240,23 @@ export async function runLoader<T>(
209
240
  theme: opts.theme,
210
241
  cacheStore: opts.cacheStore,
211
242
  cacheProfiles: opts.cacheProfiles,
243
+ stateCookie: opts.stateCookie,
212
244
  };
245
+ }
213
246
 
214
- const { ctx } = createTestRequestContext(ctxOpts);
215
-
216
- const reqCtx = ctx as RequestContext<any>;
217
-
218
- // Seed values for ctx.use(SomeHandle), matched by handle reference (so a real
219
- // handle resolves regardless of its build-injected $$id).
247
+ function runWithLoaderContext<R>(
248
+ reqCtx: RequestContext<any>,
249
+ opts: RunLoaderOptions,
250
+ fn: (ctx: TestLoaderContext) => R,
251
+ ): R {
220
252
  const handleSeeds = new Map<unknown, unknown>(opts.handles ?? []);
221
-
222
- // Tracks whether the mocked render barrier has settled. ctx.use(handle)
223
- // reads are gated on this, matching production (loader-resolution.ts).
253
+ const loaderSeeds = new Map<unknown, unknown>(opts.loaders ?? []);
224
254
  let renderedResolved = false;
225
255
 
226
256
  return runWithRequestContext(reqCtx, () => {
227
257
  const reverse = opts.routeMap
228
258
  ? createReverseFunction(opts.routeMap, opts.routeName, opts.params ?? {})
229
259
  : ((() => {
230
- // Documented contract: reverse requires routeMap. Do NOT fall back to
231
- // reqCtx.reverse (the global route map) — that leaks whichever routes
232
- // another test registered and contradicts the documented behavior.
233
260
  throw new Error(
234
261
  "ctx.reverse() requires the `routeMap` option in runLoader(). " +
235
262
  "Pass { routeMap: { name: pattern, ... } } to enable reverse().",
@@ -240,7 +267,7 @@ export async function runLoader<T>(
240
267
  params: opts.params ?? {},
241
268
  routeParams: (opts.params ?? {}) as Record<string, string>,
242
269
  request: reqCtx.request,
243
- searchParams: ctx.searchParams,
270
+ searchParams: reqCtx.searchParams,
244
271
  search: opts.searchData ?? {},
245
272
  pathname: reqCtx.pathname,
246
273
  url: reqCtx.url,
@@ -250,10 +277,6 @@ export async function runLoader<T>(
250
277
  executionContext: reqCtx.executionContext,
251
278
  get: reqCtx.get as TestLoaderContext["get"],
252
279
  use: ((dep: LoaderDefinition<any, any> | Handle<any, any>) => {
253
- // Match production (loader-resolution.ts): reading a handle in a loader
254
- // requires the render barrier to have settled. Gate BEFORE returning a
255
- // seed, so a loader that forgets `await ctx.rendered()` fails in the
256
- // test exactly as it would at runtime.
257
280
  if (isHandle(dep) && !renderedResolved) {
258
281
  throw new Error(
259
282
  `ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
@@ -261,9 +284,15 @@ export async function runLoader<T>(
261
284
  `the render tree has settled.`,
262
285
  );
263
286
  }
264
- // Handle reads (ctx.use(SomeHandle)) resolve from the seeded map first.
265
287
  if (handleSeeds.has(dep)) return handleSeeds.get(dep);
266
- if (opts.use) return opts.use(dep as LoaderDefinition<any, any>);
288
+ if (isHandle(dep)) return collectHandle(dep, []);
289
+ // Production ctx.use(Loader) ALWAYS returns a Promise (the cached loader
290
+ // promise). The seeded path must match, so a consumer composing on the
291
+ // result (ctx.use(Dep).then(...), Promise.race, etc.) works the same as
292
+ // production and the real-fn delegate path below.
293
+ if (loaderSeeds.has(dep)) return Promise.resolve(loaderSeeds.get(dep));
294
+ if (opts.use)
295
+ return Promise.resolve(opts.use(dep as LoaderDefinition<any, any>));
267
296
  return reqCtx.use(dep as LoaderDefinition<any, any>);
268
297
  }) as LoaderContext<any, any>["use"],
269
298
  method: opts.method ?? "GET",
@@ -276,7 +305,6 @@ export async function runLoader<T>(
276
305
  if (typeof opts.rendered === "function") {
277
306
  await opts.rendered();
278
307
  }
279
- // Barrier has settled: subsequent ctx.use(handle) reads resolve.
280
308
  renderedResolved = true;
281
309
  }
282
310
  : () => {
@@ -291,6 +319,67 @@ export async function runLoader<T>(
291
319
  },
292
320
  };
293
321
 
294
- return Promise.resolve(loaderFn(loaderCtx));
322
+ return fn(loaderCtx);
295
323
  });
296
324
  }
325
+
326
+ export async function runLoader<T>(
327
+ loader: RunnableLoader<T>,
328
+ opts: RunLoaderOptions = {},
329
+ ): Promise<T> {
330
+ const loaderFn = resolveLoaderFn(loader);
331
+ const { ctx } = createTestRequestContext(buildLoaderCtxOpts(opts));
332
+ return runWithLoaderContext(ctx as RequestContext<any>, opts, (loaderCtx) =>
333
+ Promise.resolve(loaderFn(loaderCtx)),
334
+ );
335
+ }
336
+
337
+ export interface RunLoaderResult<T> {
338
+ /**
339
+ * The loader's resolved data (the value bare `runLoader` returns), or
340
+ * `undefined` if it threw (see {@link thrown}). Named `result` for parity with
341
+ * `runInRequestContext`'s envelope.
342
+ */
343
+ result: T | undefined;
344
+ /**
345
+ * What the loader threw (commonly a `Response` from `throw redirect(...)` on a
346
+ * success path) — captured, NOT re-thrown; assert on it. `undefined` if the
347
+ * loader returned normally.
348
+ */
349
+ thrown: unknown;
350
+ /**
351
+ * The merged `Response` (status + headers + Set-Cookie). On a thrown redirect,
352
+ * that redirect's `Location` merged with the accumulated cookies/headers — so a
353
+ * loader that sets a session cookie then `throw redirect("/")` exposes BOTH.
354
+ */
355
+ response: Response;
356
+ /** Effective cookie view: request cookies + the loader's mutations, last-write-wins. */
357
+ cookies: Record<string, string>;
358
+ /** Response headers as `{ name: value }`, EXCLUDING set-cookie (use `cookies`). Lowercased. */
359
+ headers: Record<string, string>;
360
+ /** Location state the loader set (`ctx.setLocationState()` / `redirect({ state })`). */
361
+ locationState: Record<string, unknown>;
362
+ /** The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). */
363
+ stateCookieName: string;
364
+ }
365
+
366
+ export async function runLoaderResult<T>(
367
+ loader: RunnableLoader<T>,
368
+ opts: RunLoaderOptions = {},
369
+ ): Promise<RunLoaderResult<T>> {
370
+ const loaderFn = resolveLoaderFn(loader);
371
+ const { ctx, stateCookieName } = createTestRequestContext(
372
+ buildLoaderCtxOpts(opts),
373
+ );
374
+ const reqCtx = ctx as RequestContext<any>;
375
+ let result: T | undefined;
376
+ let thrown: unknown;
377
+ try {
378
+ result = await runWithLoaderContext(reqCtx, opts, (loaderCtx) =>
379
+ Promise.resolve(loaderFn(loaderCtx)),
380
+ );
381
+ } catch (error) {
382
+ thrown = error;
383
+ }
384
+ return { result, ...buildRunSnapshot(reqCtx, thrown, stateCookieName) };
385
+ }
@@ -24,9 +24,11 @@ import { createReverseFunction } from "../router/handler-context.js";
24
24
  import type { MiddlewareFn } from "../router/middleware-types.js";
25
25
  import {
26
26
  createTestRequestContext,
27
+ headersToObject,
27
28
  snapshotRunEffects,
28
29
  type CreateTestContextOptions,
29
30
  type VarsInit,
31
+ type StateCookieSeed,
30
32
  } from "./internal/context.js";
31
33
  import type { ThemeConfig } from "../theme/types.js";
32
34
  import type { SegmentCacheStore } from "../cache/types.js";
@@ -36,6 +38,13 @@ import type { CacheProfile } from "../cache/profile-registry.js";
36
38
  * Options for runMiddleware.
37
39
  */
38
40
  export interface RunMiddlewareOptions<TEnv = any> {
41
+ /**
42
+ * The request the chain runs under: a `Request`, or a URL string (absolute or
43
+ * path). Optional for parity with `runLoader`/`runInRequestContext` — when
44
+ * omitted it defaults to `http://localhost/`. Pass it for path-, header-, or
45
+ * cookie-driven middleware.
46
+ */
47
+ request?: Request | string;
39
48
  /** Environment bindings surfaced as `ctx.env`. */
40
49
  env?: TEnv;
41
50
  /** Route params surfaced as `ctx.params`. */
@@ -44,7 +53,12 @@ export interface RunMiddlewareOptions<TEnv = any> {
44
53
  vars?: VarsInit;
45
54
  /** Route name -> pattern map enabling `ctx.reverse()`. */
46
55
  routeMap?: Record<string, string>;
47
- /** Matched route name for scoped `.name` reverse resolution. */
56
+ /**
57
+ * Matched route name surfaced as `ctx.routeName`. Does NOT scope `.name`
58
+ * reverse: the chain receives a map-only `reverse` (built from `routeMap`
59
+ * alone), matching production app/response middleware — see the reverse
60
+ * construction below.
61
+ */
48
62
  routeName?: string;
49
63
  /** Router basename surfaced on the context (drives redirect() prefixing). */
50
64
  basename?: string;
@@ -64,6 +78,13 @@ export interface RunMiddlewareOptions<TEnv = any> {
64
78
  cacheStore?: SegmentCacheStore;
65
79
  /** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
66
80
  cacheProfiles?: Record<string, CacheProfile>;
81
+ /**
82
+ * Customize the rango state cookie a middleware that calls
83
+ * `invalidateClientCache()` rotates (the name is always seeded — default
84
+ * `rango-state_router_0` — so it rotates like production). Assert via the
85
+ * `Set-Cookie` on `result.response` / `result.cookies`.
86
+ */
87
+ stateCookie?: StateCookieSeed;
67
88
  }
68
89
 
69
90
  /**
@@ -88,34 +109,39 @@ export interface RunMiddlewareResult<TEnv = any> {
88
109
  * the `@internal` `ctx.cookies()`. Set-Cookie headers are also on `response`.
89
110
  */
90
111
  cookies: Record<string, string>;
112
+ /**
113
+ * The final response's headers as a plain `{ name: value }` object (the same
114
+ * view as `response.headers`), EXCLUDING `set-cookie` (use `cookies`). The
115
+ * public way to assert a header a middleware set (e.g. a security header)
116
+ * without reading `ctx.res.headers`. Header names are lowercased.
117
+ */
118
+ headers: Record<string, string>;
119
+ /**
120
+ * Location state the chain set via `ctx.setLocationState()` / `redirect({ state })`,
121
+ * resolved to the flat `{ key: value }` shape the client reads off
122
+ * `history.state` (empty object when none) — parity with `runInRequestContext`
123
+ * and `renderHandler`.
124
+ */
125
+ locationState: Record<string, unknown>;
126
+ /**
127
+ * The resolved rango state cookie name seeded for the run (default
128
+ * `rango-state_router_0`, or composed from `opts.stateCookie`). Assert a
129
+ * middleware's `invalidateClientCache()` rotation against it without
130
+ * recomputing — parity with `runInRequestContext` / `runLoaderResult` /
131
+ * `renderHandler`.
132
+ */
133
+ stateCookieName: string;
91
134
  }
92
135
 
93
- /**
94
- * Run a middleware chain and return the response plus observable context.
95
- *
96
- * @example
97
- * ```ts
98
- * const { response, ctx, nextCalled } = await runMiddleware(
99
- * async (ctx, next) => {
100
- * if (!ctx.get("user")) return new Response(null, { status: 401 });
101
- * return next();
102
- * },
103
- * "/dashboard",
104
- * { vars: [["user", { id: 1 }]] },
105
- * );
106
- * // nextCalled === 1, response.status === 200
107
- * ```
108
- */
109
136
  export async function runMiddleware<TEnv = any>(
110
137
  mw: MiddlewareFn<TEnv> | MiddlewareFn<TEnv>[],
111
- request: Request | string,
112
- opts: RunMiddlewareOptions<TEnv> = {},
138
+ opts: RunMiddlewareOptions<TEnv>,
113
139
  ): Promise<RunMiddlewareResult<TEnv>> {
114
140
  const mwArray = Array.isArray(mw) ? mw : [mw];
115
141
 
116
142
  const ctxOpts: CreateTestContextOptions<TEnv> = {
117
143
  env: opts.env,
118
- request,
144
+ request: opts.request,
119
145
  vars: opts.vars,
120
146
  routeMap: opts.routeMap,
121
147
  routeName: opts.routeName,
@@ -124,12 +150,14 @@ export async function runMiddleware<TEnv = any>(
124
150
  theme: opts.theme,
125
151
  cacheStore: opts.cacheStore,
126
152
  cacheProfiles: opts.cacheProfiles,
153
+ stateCookie: opts.stateCookie,
127
154
  };
128
155
 
129
156
  const {
130
157
  ctx,
131
158
  request: builtRequest,
132
159
  variables,
160
+ stateCookieName,
133
161
  } = createTestRequestContext<TEnv>(ctxOpts);
134
162
 
135
163
  let nextCalled = 0;
@@ -138,11 +166,6 @@ export async function runMiddleware<TEnv = any>(
138
166
  return opts.next?.() ?? new Response(null, { status: 200 });
139
167
  };
140
168
 
141
- // Match production: app/response middleware receive ctx.reverse built from the
142
- // route map ALONE (no matched route name or current params), so reversing a
143
- // parameterized route without explicit params does NOT auto-fill from the
144
- // current request. Passing routeName/params here would recreate the
145
- // false-confidence class fixed in dispatch.
146
169
  const reverse = opts.routeMap
147
170
  ? (createReverseFunction(opts.routeMap) as (
148
171
  name: string,
@@ -151,12 +174,6 @@ export async function runMiddleware<TEnv = any>(
151
174
  ) => string)
152
175
  : undefined;
153
176
 
154
- // Keep the RETURNED ctx.reverse consistent with the map-only reverse the
155
- // chain receives. createTestRequestContext installs an auto-fill reverse
156
- // (correct for the loader phase) when routeName/params are passed, but
157
- // production app/response middleware see a map-only reverse. Without this,
158
- // a middleware reading getRequestContext().reverse — or a consumer asserting
159
- // on result.ctx.reverse — would observe auto-fill that production never does.
160
177
  if (reverse) {
161
178
  (ctx as RequestContext<TEnv>).reverse =
162
179
  reverse as RequestContext<TEnv>["reverse"];
@@ -174,6 +191,15 @@ export async function runMiddleware<TEnv = any>(
174
191
  ),
175
192
  );
176
193
 
177
- const { cookies } = snapshotRunEffects(ctx);
178
- return { response, ctx, nextCalled, cookies };
194
+ const { cookies, locationState } = snapshotRunEffects(ctx);
195
+ const headers = headersToObject(response.headers);
196
+ return {
197
+ response,
198
+ ctx,
199
+ nextCalled,
200
+ cookies,
201
+ headers,
202
+ locationState,
203
+ stateCookieName,
204
+ };
179
205
  }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * runTransitionWhen — unit-test a transition({ when }) predicate in isolation.
3
+ *
4
+ * Runs the SAME two server functions the router uses — applyViewTransitionDefault
5
+ * (strips the `when` function from the serialized config and records the
6
+ * predicate on the request context) and gateTransitions (assembles the
7
+ * TransitionWhenContext and evaluates the predicate post-handler). So the
8
+ * predicate sees exactly the navigation/action metadata it would at runtime
9
+ * (currentUrl/currentParams/fromRouteName, nextUrl/nextParams/toRouteName,
10
+ * actionId/actionUrl/actionResult/formData/method, get/env), and `kept` reflects
11
+ * whether the transition would apply this request. The result also exposes the
12
+ * assembled `whenContext` so tests can assert the exact fields without reaching
13
+ * into private request-context state.
14
+ *
15
+ * This is the public way to exercise a transition gate: the full
16
+ * match -> render pipeline that wires these together only runs under real RSC
17
+ * rendering (which the Flight primitives do not drive), so without this primitive
18
+ * a consumer could not test their predicate through @rangojs/router/testing.
19
+ *
20
+ * Synchronous: a transition predicate returns a boolean and the gate has no I/O.
21
+ */
22
+
23
+ import {
24
+ runWithRequestContext,
25
+ type RequestContext,
26
+ } from "../server/request-context.js";
27
+ import { applyViewTransitionDefault } from "../router/segment-resolution/view-transition-default.js";
28
+ import { gateTransitions } from "../rsc/transition-gate.js";
29
+ import { createTestRequestContext, type VarsInit } from "./internal/context.js";
30
+ import type {
31
+ ResolvedSegment,
32
+ TransitionConfig,
33
+ TransitionWhenContext,
34
+ } from "../types/segments.js";
35
+ import type { OnErrorCallback } from "../types/error-types.js";
36
+
37
+ const toURL = (v: string | URL, base: URL): URL =>
38
+ typeof v === "string" ? new URL(v, base.origin) : v;
39
+
40
+ /**
41
+ * Options for runTransitionWhen. All navigation/action fields are optional and
42
+ * default to "absent", matching what the gate sees for an initial full load with
43
+ * no action: omit `currentUrl`/`currentParams`/`fromRouteName` to model the
44
+ * navigation source being unavailable, and omit the `action*` fields to model a
45
+ * plain (non-action) navigation.
46
+ */
47
+ export interface RunTransitionWhenOptions<TEnv = any> {
48
+ /** The navigation TARGET request (drives `nextUrl`): a Request or URL/path string. Defaults to `http://localhost/`. */
49
+ request?: Request | string;
50
+ /** Route params for the target (`nextParams`). */
51
+ params?: Record<string, string>;
52
+ /** Target route name (`toRouteName`). */
53
+ toRouteName?: string;
54
+ /** Environment bindings surfaced as `env` (and `ctx.env`). */
55
+ env?: TEnv;
56
+ /** Variables a handler/middleware would have set this request, readable via the predicate's `get()`. */
57
+ vars?: VarsInit;
58
+ /** Navigation SOURCE url (`currentUrl`): a URL or path string. */
59
+ currentUrl?: string | URL;
60
+ /** Source route params (`currentParams`). */
61
+ currentParams?: Record<string, string>;
62
+ /** Source route name (`fromRouteName`). */
63
+ fromRouteName?: string;
64
+ /** Id of the action that triggered a revalidation (`actionId`). */
65
+ actionId?: string;
66
+ /** Url the action was submitted from (`actionUrl`). */
67
+ actionUrl?: string | URL;
68
+ /** The action's return value (`actionResult`). */
69
+ actionResult?: unknown;
70
+ /** FormData from a form action (`formData`). */
71
+ formData?: FormData;
72
+ /** Receives an error thrown by the predicate (the gate reports to `router.onError`, phase `"rendering"`). */
73
+ onError?: OnErrorCallback;
74
+ }
75
+
76
+ /**
77
+ * Result of runTransitionWhen.
78
+ */
79
+ export interface RunTransitionWhenResult<TEnv = any> {
80
+ /** True if the transition would apply this request (predicate returned non-false, or there is no `when`). */
81
+ kept: boolean;
82
+ /** Convenience inverse of `kept`. */
83
+ dropped: boolean;
84
+ /**
85
+ * The production-assembled predicate context. Undefined when the config has
86
+ * no `when` predicate.
87
+ */
88
+ whenContext?: TransitionWhenContext<Record<string, string>, TEnv>;
89
+ /** The underlying RequestContext, for additional assertions (`ctx.get(...)`, etc.). */
90
+ ctx: RequestContext<TEnv>;
91
+ }
92
+
93
+ export function runTransitionWhen<TEnv = any>(
94
+ config: TransitionConfig,
95
+ opts: RunTransitionWhenOptions<TEnv> = {},
96
+ ): RunTransitionWhenResult<TEnv> {
97
+ const { ctx } = createTestRequestContext<TEnv>({
98
+ env: opts.env,
99
+ request: opts.request,
100
+ vars: opts.vars,
101
+ params: opts.params,
102
+ });
103
+ const reqCtx = ctx as unknown as RequestContext<TEnv>;
104
+
105
+ // Target route name (the public field the gate reads for `toRouteName`).
106
+ if (opts.toRouteName !== undefined)
107
+ reqCtx.routeName = opts.toRouteName as RequestContext<TEnv>["routeName"];
108
+ // Source (match-time) data the gate reads for currentUrl/currentParams/fromRouteName.
109
+ if (opts.currentUrl !== undefined)
110
+ reqCtx._gateCurrentUrl = toURL(opts.currentUrl, reqCtx.url);
111
+ if (opts.currentParams !== undefined)
112
+ reqCtx._gateCurrentParams = opts.currentParams;
113
+ if (opts.fromRouteName !== undefined)
114
+ reqCtx._prevRouteKey = opts.fromRouteName;
115
+ // Action data the gate reads at the action-bearing call sites.
116
+ if (opts.actionId !== undefined) reqCtx._gateActionId = opts.actionId;
117
+ if (opts.actionUrl !== undefined)
118
+ reqCtx._gateActionUrl = toURL(opts.actionUrl, reqCtx.url);
119
+ if (opts.actionResult !== undefined)
120
+ reqCtx._gateActionResult = opts.actionResult;
121
+ if (opts.formData !== undefined) reqCtx._gateFormData = opts.formData;
122
+
123
+ let whenContext:
124
+ | TransitionWhenContext<Record<string, string>, TEnv>
125
+ | undefined;
126
+ const when = config.when;
127
+ const configForGate: TransitionConfig = when
128
+ ? {
129
+ ...config,
130
+ when: (c) => {
131
+ whenContext = c as TransitionWhenContext<
132
+ Record<string, string>,
133
+ TEnv
134
+ >;
135
+ return when(c);
136
+ },
137
+ }
138
+ : config;
139
+
140
+ return runWithRequestContext(reqCtx, () => {
141
+ // The real resolution-time collection + post-handler gate, so the predicate
142
+ // sees the production-assembled TransitionWhenContext.
143
+ const serialized = applyViewTransitionDefault(
144
+ configForGate,
145
+ undefined,
146
+ "tx-when-seg",
147
+ );
148
+ const segment = {
149
+ id: "tx-when-seg",
150
+ namespace: "r",
151
+ type: "route",
152
+ index: 0,
153
+ component: null,
154
+ transition: serialized,
155
+ } as ResolvedSegment;
156
+ gateTransitions(
157
+ [segment],
158
+ reqCtx as Parameters<typeof gateTransitions>[1],
159
+ opts.onError,
160
+ );
161
+ const kept = segment.transition !== undefined;
162
+ return { kept, dropped: !kept, whenContext, ctx: reqCtx };
163
+ });
164
+ }
@@ -1,5 +1,5 @@
1
1
  // Stub for the `cloudflare:email` runtime virtual, shipped for Cloudflare
2
- // consumers (enable via `rangoTestAliases({ cloudflare: true })`).
2
+ // consumers (enable via `rangoTestAliases({ preset: "cloudflare" })`).
3
3
  export class EmailMessage {
4
4
  constructor(
5
5
  public from: string,
@@ -1,5 +1,5 @@
1
1
  // Stub for the `cloudflare:workers` runtime virtual, shipped for Cloudflare
2
- // consumers (enable via `rangoTestAliases({ cloudflare: true })`). A CF app's
2
+ // consumers (enable via `rangoTestAliases({ preset: "cloudflare" })`). A CF app's
3
3
  // route tree commonly imports `cloudflare:workers` (e.g. `import { env } from
4
4
  // "cloudflare:workers"`), which does not resolve in a bare Vitest process.
5
5
  export const env: Record<string, unknown> = {};