@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,128 @@
1
+ # Testing a loader — runLoader
2
+
3
+ **Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `loader()` (see `/loader`)
4
+
5
+ `runLoader` runs a loader against a real `RequestContext` (cookies, headers, `ctx.get`, `ctx.reverse` all resolve) in plain node — that machinery is REAL; what you SEED is the params, env, vars, search, route map, and any `ctx.use` dependency data. Pass a registered `createLoader()` handle (its fn is recovered from the registry) or the raw async body `(ctx) => ...`.
6
+
7
+ ## API
8
+
9
+ ### Options — `RunLoaderOptions<TEnv>`
10
+
11
+ | Field | Type | Meaning |
12
+ | --------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `params` | `Record<string, string>` | Route params; surfaced as `ctx.params` and `ctx.routeParams`. |
14
+ | `search` | `Record<string, string>` | Search params; merged into the request URL so `ctx.searchParams` reflects them. |
15
+ | `searchData` | `Record<string, unknown>` | The TYPED `ctx.search` object a route's search schema would produce. Distinct from `search` (which sets the raw `ctx.searchParams`). |
16
+ | `basename` | `string` | Router basename surfaced on the context; drives `redirect()` prefixing. |
17
+ | `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape (e.g. `true` or `{ themes: [...] }`). Without it `ctx.theme`/`ctx.setTheme` are inert. |
18
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
19
+ | `request` | `Request \| string` | Override the backing Request. Defaults to a localhost GET. |
20
+ | `vars` | `VarsInit` | Variables a prior middleware would have set (object `{ key: value }`, or `[key, value]` tuples where the key may be a `createVar()` handle). |
21
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
+ | `routeName` | `string` | Matched route name for scoped `.name` reverse resolution. |
23
+ | `method` | `string` | HTTP method surfaced as `ctx.method`. Defaults to `"GET"`. |
24
+ | `body` | `unknown` | Request body surfaced as `ctx.body`. |
25
+ | `formData` | `FormData` | Form data surfaced as `ctx.formData` (exposed verbatim; no multipart parsing). |
26
+ | `loaders` | `ReadonlyArray<readonly [LoaderDefinition<any, any>, unknown]>` | Seed `ctx.use(OtherLoader)` by REFERENCE as `[[OtherLoader, data]]` tuples (same shape as `renderHandler`/`renderRoute`). Checked before `use`. |
27
+ | `use` | `UseResolver` | Dynamic resolver for `ctx.use(OtherLoader)` composition. `loaders` wins when both match. |
28
+ | `cacheStore` | `SegmentCacheStore` | Cache store backing `use cache` functions. Without one, a cached function bypasses and runs uncached (its taint/profile guards never fire). |
29
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles, the `createRouter({ cacheProfiles })` shape. |
30
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a loader calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). |
31
+ | `rendered` | `boolean \| (() => void \| Promise<void>)` | Mock the `ctx.rendered()` render barrier so a loader that `await ctx.rendered()`s can be unit-tested. By default `ctx.rendered()` throws. `true` resolves immediately; a function controls timing/side effects. |
32
+ | `handles` | `ReadonlyArray<readonly [Handle<any, any>, unknown]>` | Seed the values `ctx.use(SomeHandle)` returns — the ACCUMULATED handle data read after `await ctx.rendered()`. Matched by handle reference. |
33
+
34
+ ### Context — `TestLoaderContext<TEnv>` (what your loader receives)
35
+
36
+ | Field | Type | Meaning |
37
+ | ------------------ | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
38
+ | `params` | `Record<string, string>` | Route params (from `opts.params`). |
39
+ | `routeParams` | `Record<string, string>` | Same values as `params`. |
40
+ | `request` | `Request` | The backing request. |
41
+ | `searchParams` | `URLSearchParams` | Raw search params (from `opts.search` baked into the URL). |
42
+ | `search` | `Record<string, unknown>` | The TYPED search object (from `opts.searchData`); defaults to `{}`. |
43
+ | `pathname` | `string` | Request pathname. |
44
+ | `url` | `URL` | Request URL. |
45
+ | `originalUrl` | `URL` | Pre-basename-rewrite URL. |
46
+ | `env` | `TEnv` | Environment bindings (from `opts.env`). |
47
+ | `get` | `<T>(contextVar: ContextVar<T>) => T \| undefined` / `<T>(key: string) => T \| undefined` | Read a var seeded via `opts.vars` (by `createVar()` handle or string key). |
48
+ | `use` | `(dep) => ...` | Resolve `ctx.use(OtherLoader)`/`ctx.use(SomeHandle)`: handle seeds first, then loader seeds, then the `use` resolver, then the real context `use()`. |
49
+ | `method` | `string` | HTTP method (from `opts.method`, default `"GET"`). |
50
+ | `body` | `unknown` | Request body (from `opts.body`). |
51
+ | `formData` | `FormData \| undefined` | Form data (from `opts.formData`). |
52
+ | `reverse` | `(name, params?, search?) => string` | Build a URL; throws unless `opts.routeMap` was passed. |
53
+ | `rendered` | `() => Promise<void>` | The render barrier; throws by default, mocked via `opts.rendered`. |
54
+ | `waitUntil` | `(p: Promise<unknown>) => void` | Register background work (no-op accounting in tests). |
55
+ | `executionContext` | `ExecutionContext \| undefined` | Platform execution context from the backing request; pairs with `waitUntil`. |
56
+
57
+ ### Returns — `Promise<T>`
58
+
59
+ The loader data DIRECTLY (no envelope). `T` is the loader's return type.
60
+
61
+ To assert a loader's EFFECTS — a `Set-Cookie`, a response header, or a
62
+ `throw redirect(...)` (the auth-loader pattern) — use the sibling
63
+ **`runLoaderResult(loader, opts)`** instead. Same options, but it returns an
64
+ envelope: `{ result, thrown, response, cookies, headers, locationState, stateCookieName }`
65
+ (parity with `runInRequestContext`; `result` is the loader's data). `runLoader`
66
+ discards those effects.
67
+
68
+ ## Recipe
69
+
70
+ ```ts
71
+ import { runLoader, runLoaderResult } from "@rangojs/router/testing";
72
+ import { createLoader, createVar } from "@rangojs/router";
73
+
74
+ const User = createVar<{ name: string }>();
75
+ // The registered loader — no separate body export needed for testability:
76
+ const ProductLoader = createLoader(async (ctx) => ({
77
+ id: ctx.params.id,
78
+ region: ctx.env.REGION,
79
+ user: ctx.get(User),
80
+ }));
81
+
82
+ it("reads params, env, and seeded vars", async () => {
83
+ const data = await runLoader(ProductLoader, {
84
+ params: { id: "42" },
85
+ env: { REGION: "eu" },
86
+ vars: [[User, { name: "Ada" }]],
87
+ });
88
+ expect(data).toEqual({ id: "42", region: "eu", user: { name: "Ada" } });
89
+ });
90
+
91
+ it("builds a self link via reverse", async () => {
92
+ // runLoader(async (ctx) => ({ ... }), opts) — the bare body — works identically.
93
+ const data = await runLoader(
94
+ async (ctx) => ({ self: ctx.reverse("product", { id: ctx.params.id }) }),
95
+ { params: { id: "42" }, routeMap: { product: "/products/:id" } },
96
+ );
97
+ expect(data.self).toBe("/products/42");
98
+ });
99
+
100
+ it("asserts a loader's set-cookie + redirect (runLoaderResult)", async () => {
101
+ // AuthLoader validates, sets a `session` cookie, then `throw redirect("/")`.
102
+ const { thrown, response, cookies } = await runLoaderResult(AuthLoader, {
103
+ request: new Request("https://app.test/login?token=ok"),
104
+ });
105
+ expect((thrown as Response).headers.get("Location")).toBe("/");
106
+ expect(cookies.session).toBeDefined();
107
+ expect(
108
+ response.headers.getSetCookie().some((c) => c.startsWith("session=")),
109
+ ).toBe(true);
110
+ });
111
+ ```
112
+
113
+ ## Caveats
114
+
115
+ - `ctx.reverse(...)` throws unless you pass `routeMap` (and `routeName` for scoped `.name` resolution). It does NOT fall back to the global route map.
116
+ - `ctx.rendered()` throws by default (the render barrier only exists in a full match); pass `{ rendered: true }` to mock it for post-barrier logic, and `{ handles: [[SomeHandle, data]] }` to seed `ctx.use(SomeHandle)`. `ctx.isAction(...)` is unavailable — cover those at e2e.
117
+ - Seeded `loaders` (by-reference tuples) are NOT executed — `ctx.use(OtherLoader)` returns the seeded value. The dynamic `use` resolver, by contrast, IS executed (it is a function called to compute the value). Either way the REAL loader body is not run; real loader execution and side-effects are e2e-only. `loaders` is checked before the `use` resolver.
118
+ - A handle imported through the CLIENT build has its body dropped — `runLoader` throws a clear error pointing to the `rangoTestConfig()` preset or the raw body. A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`); only the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules).
119
+ - No `cookies`/`headers` option: seed a cookie by passing a full Request with a Cookie header — `{ request: new Request(url, { headers: { Cookie: "sid=abc" } }) }`. (`search`/`method` are baked onto this request for you.)
120
+ - `ctx.search` (typed) defaults to `{}`; `opts.search` only sets the raw `ctx.searchParams`. Seed the typed object with `searchData`. (The harness seeds `searchData` verbatim — it does NOT run a typed-search SCHEMA, so schema parsing/validation is e2e.)
121
+ - `ctx.theme`/`ctx.setTheme` are NOT on the loader context — theme accessors are handler-only. (The `theme` option seeds the underlying request context for `use cache` theme resolution, but a loader body cannot read theme.) `redirect()` does no basename prefixing unless you seed `basename`.
122
+ - Platform bindings are yours to double via `env` (see `./bindings.md`).
123
+
124
+ ## See also
125
+
126
+ - `/loader` — the DSL this tests
127
+ - Siblings: `./handles.md`, `./reverse-and-types.md`, `./bindings.md`, `./server-actions.md`
128
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Loaders — the raw body or a registered createLoader"
@@ -0,0 +1,99 @@
1
+ # Testing middleware — runMiddleware
2
+
3
+ **Layer:** unit (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** `middleware()` (see `/middleware`)
4
+
5
+ `runMiddleware` executes your chain through the router's REAL `executeLoaderMiddleware`, so `next()`, return-Response and throw-Response short-circuits, double-next guards, and header/cookie merge are production-identical. You SEED the request and any prior-middleware state (`vars`, `params`, `env`, `routeMap`); everything else (cookie/header merge, request-context resolution) is real machinery.
6
+
7
+ ## API
8
+
9
+ ### Options — `RunMiddlewareOptions<TEnv>`
10
+
11
+ | Field | Type | Meaning |
12
+ | --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `request` | `Request \| string` | The request the chain runs under: a `Request`, or a URL string (absolute or path). Optional — defaults to `http://localhost/`; pass it for path-, header-, or cookie-driven middleware. |
14
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. Your seam for doubling platform bindings (see `./bindings.md`). |
15
+ | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
16
+ | `vars` | `VarsInit` | Variables a prior middleware would have set (object form, or `[key, value]` tuples where `key` may be a `createVar()` handle). |
17
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
18
+ | `routeName` | `string` | Matched route name surfaced as `ctx.routeName`. Does NOT enable scoped `.name` reverse: the chain's `reverse` is deliberately map-only, matching production app/response middleware. |
19
+ | `basename` | `string` | Router basename surfaced on the context (drives `redirect()` prefixing). |
20
+ | `theme` | `ThemeConfig \| true` | Theme config in the `createRouter({ theme })` shape; enables `ctx.theme`. |
21
+ | `next` | `() => Promise<Response>` | Terminal handler invoked when the chain calls `next()` all the way through. Defaults to a 200 empty Response. Use it to model the downstream route/handler response. |
22
+ | `cacheStore` | `SegmentCacheStore` | Cache store backing any `use cache` function a middleware invokes. Without it, `registerCachedFunction` bypasses, so the cached fn runs uncached and its taint/profile guards never fire. |
23
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape. |
24
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a middleware calling `invalidateClientCache()` rotates (the name is always seeded — default `rango-state_router_0`). Assert via the `Set-Cookie` on `result.response` / `result.cookies`, or against `result.stateCookieName` (without recomputing). |
25
+
26
+ ### Context — `MiddlewareContext` (what your code receives)
27
+
28
+ The `ctx` your middleware reads. Notable fields:
29
+
30
+ | Field | Type | Meaning |
31
+ | --------------------------- | ----------------------- | ---------------------------------------------------------------------------------- |
32
+ | `params` | `TParams` | URL params from `opts.params`. |
33
+ | `env` | `TEnv` | Bindings from `opts.env`. |
34
+ | `get` / `set` | fns | Read/write context vars (shared with handlers); `get` resolves what `vars` seeded. |
35
+ | `header(name, value)` | fn | Queue a response header before `next()`, or set it directly after. |
36
+ | `reverse` | `ScopedReverseFunction` | URL-from-name. Map-only (no auto-fill); needs `routeMap`. |
37
+ | `setLocationState(entries)` | fn | Attach flash/location state to the response. |
38
+ | `theme` / `setTheme` | `Theme` / fn | Current theme; `undefined` unless `theme` is passed. |
39
+ | `routeName` | `string` | Matched route name (from `opts.routeName`). |
40
+
41
+ ### Returns — `RunMiddlewareResult<TEnv>`
42
+
43
+ | Field | Type | Meaning |
44
+ | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
45
+ | `response` | `Response` | The final Response: the downstream response, or a middleware short-circuit. |
46
+ | `ctx` | `RequestContext<TEnv>` | The underlying RequestContext (NOT a per-middleware `MiddlewareContext`). Use `ctx.get(...)` for anything the envelope above doesn't surface. |
47
+ | `nextCalled` | `number` | Times the terminal handler ran: `0` on short-circuit, `1` on pass-through. |
48
+ | `cookies` | `Record<string, string>` | Effective cookie view: request cookies merged with chain sets/deletes (last-write-wins), as `{ name: value }`. |
49
+ | `headers` | `Record<string, string>` | Final response headers as `{ name: value }`, lowercased, EXCLUDING `set-cookie` (use `cookies`). |
50
+ | `locationState` | `Record<string, unknown>` | Flat `{ key: value }` state set via `setLocationState()` / `redirect({ state })` (empty when none). |
51
+ | `stateCookieName` | `string` | The resolved rango state cookie name seeded for the run (default `rango-state_router_0`). Assert a middleware's `invalidateClientCache()` rotation against it without recomputing — parity with `runInRequestContext` / `runLoaderResult` / `renderHandler`. |
52
+
53
+ ## Recipe
54
+
55
+ ```ts
56
+ import { describe, it, expect } from "vitest";
57
+ import { runMiddleware } from "@rangojs/router/testing";
58
+ import type { Middleware } from "@rangojs/router";
59
+
60
+ const requireUser: Middleware = async (ctx, next) => {
61
+ if (!ctx.get("user")) return new Response(null, { status: 401 });
62
+ return next();
63
+ };
64
+
65
+ describe("requireUser", () => {
66
+ it("passes through when the user is present", async () => {
67
+ const { response, nextCalled } = await runMiddleware(requireUser, {
68
+ request: "/dashboard",
69
+ vars: { user: { id: 1 } }, // object form; or [[key, value]] tuples (key may be a createVar())
70
+ });
71
+ expect(nextCalled).toBe(1);
72
+ expect(response.status).toBe(200);
73
+ });
74
+
75
+ it("short-circuits (return OR throw Response) when unauthenticated", async () => {
76
+ const { response, nextCalled } = await runMiddleware(requireUser, {
77
+ request: "/dashboard",
78
+ });
79
+ expect(nextCalled).toBe(0);
80
+ expect(response.status).toBe(401);
81
+ });
82
+ });
83
+ ```
84
+
85
+ Pass an array to run several in order. Cookies set inside middleware via the standalone `cookies().set(...)` (imported from `@rangojs/router`, NOT a `ctx` method) surface on the result's `cookies` and on the merged response `Set-Cookie`.
86
+
87
+ ## Caveats
88
+
89
+ - No `handles`/`rendered` option by design: middleware runs BEFORE the render barrier, so it has no post-barrier `ctx.use(Handle)` access in production. Read handle data in a loader/handler and test it with `runLoader` (see `./handles.md`).
90
+ - A COMPONENT route's guard stack cannot be exercised through `dispatch` (it throws on component routes), and `renderToFlightString`/`renderRoute` don't run route middleware. Extract the middleware fn and unit-test it here, or assert the guard stack at e2e.
91
+ - Middleware-phase `ctx.reverse` is map-only (no auto-fill from current params), matching production — enable it with `routeMap`. `routeName` only feeds `ctx.routeName`; it does NOT scope `.name` reverse (the chain reverse stays map-only by design).
92
+ - `ctx.theme` is `undefined` unless `theme` is passed; `redirect()` does no basename prefixing unless `basename` is seeded.
93
+ - Platform bindings are yours to double via `env` (see `./bindings.md`).
94
+
95
+ ## See also
96
+
97
+ - `/middleware` — the DSL this tests
98
+ - Siblings: `./response-routes.md`, `./server-actions.md`, `./loader.md`, `./bindings.md`
99
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Middleware"
@@ -0,0 +1,121 @@
1
+ # Testing a route handler — renderHandler
2
+
3
+ **Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` · **DSL it tests:** a route handler `(ctx) => rsc` (see `/route`)
4
+
5
+ A Rango route handler is a pure function `(ctx) => rsc` — the function you pass to `path("/p/:slug", ProductPage)`, NOT a component. `renderHandler` runs it with the REAL `HandlerContext` the router builds at runtime (so `ctx.params`, `ctx.use(Loader)`, `ctx.use(Meta)`, `ctx.reverse`, `ctx.get`, response headers via `ctx.headers`, and the standalone `cookies()` all work), serializes the returned RSC, and deserializes it to an inspectable tree. The render and effects are real; loaders are SEEDED (no real loader runs — same model as `runLoader`).
6
+
7
+ ## API
8
+
9
+ ### Options — `RenderHandlerOptions`
10
+
11
+ | Field | Type | Meaning |
12
+ | ------------------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
13
+ | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
14
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
15
+ | `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
16
+ | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. |
17
+ | `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
18
+ | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
19
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
20
+ | `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
21
+ | `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
22
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (the value is `{version}:{timestamp}`, default `"0"`). |
23
+ | `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
24
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
25
+
26
+ ### Context — `HandlerContext` (what your handler receives)
27
+
28
+ | Field | Type | Meaning |
29
+ | ------------------ | ------------------------------------ | ---------------------------------------------------------------------------------------------- |
30
+ | `params` | `Record<string, string>` | The seeded route params. |
31
+ | `env` | `TEnv` | The seeded environment bindings. |
32
+ | `request` | `Request` | The backing request. |
33
+ | `searchParams` | `URLSearchParams` | Parsed query of `request.url`. |
34
+ | `pathname` | `string` | Pathname of `request.url`. |
35
+ | `url` | `URL` | Parsed `request.url`. |
36
+ | `routeName` | `string \| undefined` | The matched route name (from `routeName`). |
37
+ | `use` | `(loaderOrHandle) => data \| pushFn` | A loader returns its seeded data; a handle returns a push fn that RECORDS to `result.handles`. |
38
+ | `reverse` | `(name, params?) => string` | Build a URL from `routeMap`. |
39
+ | `get` | `(Var) => value` | Read a seeded `vars` variable. |
40
+ | `headers` | `Headers` | Response headers; set via `ctx.headers.set(...)` (merged into `result.response`). |
41
+ | `setLocationState` | `(entries) => void` | Set location state (surfaced on `result.locationState`). |
42
+ | `waitUntil` | `(promise) => void` | Register background work. |
43
+
44
+ ### Returns — `RenderHandlerResult`
45
+
46
+ | Field | Type | Meaning |
47
+ | ----------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `tree` | `unknown` | Deserialized RSC the handler returned; `undefined` when it returned/threw a `Response`. Inspect with `findClientBoundaries`. |
49
+ | `flight` | `string \| undefined` | Raw Flight wire string; `undefined` on a `Response`. |
50
+ | `thrown` | `unknown` | The value the handler THREW (a `redirect()`/`notFound()` Response), captured not re-thrown. |
51
+ | `response` | `Response` | Merged Response (status + headers + Set-Cookie), folding a thrown/returned redirect with accumulated effects. |
52
+ | `cookies` | `Record<string, string>` | Effective cookie view after the handler ran. |
53
+ | `headers` | `Record<string, string>` | Response headers (excludes set-cookie; includes a redirect `Location`). The `keepClientCache()` directive shows here as `x-rango-keep-cache: "1"`. |
54
+ | `stateCookieName` | `string` | The resolved rango state cookie name this run seeded (default `rango-state_router_0`). Assert an `invalidateClientCache()` rotation against it without recomputing. |
55
+ | `locationState` | `Record<string, unknown>` | Location state the handler set (`ctx.setLocationState`/`redirect({ state })`). |
56
+ | `handles` | `Map<Handle, unknown[]>` | What the handler pushed via `ctx.use(Handle)(...)` (e.g. `Meta`, `Breadcrumbs`), keyed by handle. |
57
+
58
+ ## Recipe
59
+
60
+ ```tsx
61
+ import {
62
+ renderHandler,
63
+ findClientBoundaries,
64
+ } from "@rangojs/router/testing/flight";
65
+ import { ProductPage } from "../src/pages/product"; // the real handler: (ctx) => rsc
66
+ import { ProductLoader } from "../src/loaders/product";
67
+ import { Tenant } from "../src/middleware/tenant";
68
+ import { Meta } from "../src/handles";
69
+
70
+ it("renders the product page for a tenant", async () => {
71
+ const { tree, handles } = await renderHandler(ProductPage, {
72
+ params: { slug: "wine" },
73
+ loaders: [[ProductLoader, { name: "Wine", price: 9 }]], // seeds ctx.use(ProductLoader)
74
+ vars: [[Tenant, { name: "Acme" }]], // seeds ctx.get(Tenant)
75
+ routeMap: { product: "/p/:slug" }, // enables ctx.reverse
76
+ });
77
+
78
+ expect(JSON.stringify(tree)).toContain("Wine");
79
+ const [counter] = findClientBoundaries(tree, "Counter"); // islands inspectable too
80
+ expect(handles.get(Meta)).toEqual([{ title: "Wine - Shop" }]); // ctx.use(Meta) pushes
81
+ });
82
+
83
+ it("captures a guarded redirect", async () => {
84
+ const { thrown, response } = await renderHandler(ProductPage, {
85
+ params: { slug: "missing" },
86
+ loaders: [[ProductLoader, null]],
87
+ });
88
+
89
+ expect(thrown).toBeInstanceOf(Response); // throw redirect() is captured, not re-thrown
90
+ expect(response.status).toBe(302);
91
+ });
92
+
93
+ it("asserts the client-cache directives", async () => {
94
+ // invalidateClientCache() rotates the state cookie -> a Set-Cookie on response.
95
+ const { response, stateCookieName } = await renderHandler(LogoutPage);
96
+ expect(
97
+ response.headers
98
+ .getSetCookie()
99
+ .some((c) => c.startsWith(stateCookieName + "=")),
100
+ ).toBe(true);
101
+
102
+ // keepClientCache() sets the suppression directive header (no cookie).
103
+ const { headers } = await renderHandler(QuietPage);
104
+ expect(headers["x-rango-keep-cache"]).toBe("1");
105
+ });
106
+ ```
107
+
108
+ ## Caveats
109
+
110
+ - An unseeded `ctx.use(loader)` REJECTS with a setup error — seed every dependency via `{ loaders: [[OtherLoader, data]] }`, matched by reference. Loaders are SEEDED, not executed (same as `runLoader`).
111
+ - Same alias requirement as flight tests: without the `@rangojs/router -> index.rsc.ts` alias (see [`./setup.md`](./setup.md)), a handler reading `getRequestContext()`/`cookies()` hits the throwing out-of-react-server stub. Symptom: `tree: undefined` with the stub error on `thrown`.
112
+ - A `throw redirect()` is captured on `thrown` (with `tree` undefined, since it produced a `Response`) — assert on `thrown`/`response`, no try/catch needed.
113
+ - No hydration and no interaction — for clicks, forms, and navigation use e2e.
114
+ - `renderHandler` runs a handler FUNCTION `(ctx) => rsc`; for a plain ELEMENT `<Page/>` use `renderServerTree` (see [`./server-tree.md`](./server-tree.md)).
115
+ - A handler that calls a `"use cache"` function runs UNCACHED unless you seed `cacheStore` (and `cacheProfiles` for a named profile). With nothing seeded the runtime bypasses to the live body and warns once under the test runner — assert real cache behavior by passing `{ cacheStore: new MemorySegmentCacheStore(), cacheProfiles: { default: { ttl: 60 } } }`.
116
+
117
+ ## See also
118
+
119
+ - `/route` — the DSL this tests
120
+ - Siblings: [`./server-tree.md`](./server-tree.md), [`./server-actions.md`](./server-actions.md), [`./setup.md`](./setup.md), [`./loader.md`](./loader.md)
121
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "renderHandler — run a real route handler and assert its RSC"
@@ -0,0 +1,95 @@
1
+ # Testing a response route / redirect — dispatch
2
+
3
+ **Layer:** integration (node) · **Import:** `@rangojs/router/testing` · **DSL it tests:** response routes (json/text/html/xml/md), redirects, 404 (see `/response-routes`, `/mime-routes`)
4
+
5
+ `dispatch` runs the router's REAL matching (reusing `previewMatch`) and the real global + route-level middleware chain, with no RSC render — so redirects, 404s, response routes, content negotiation, and middleware short-circuits behave exactly as in production. You SEED the request and `env`; everything else (matching, middleware, header/cookie merge) is real machinery.
6
+
7
+ ## API
8
+
9
+ ### Options — `DispatchOptions<TEnv>`
10
+
11
+ | Field | Type | Meaning |
12
+ | -------------------- | ------------------- | ---------------------------------------------------------------------------------- |
13
+ | `request` (required) | `Request \| string` | The request to dispatch: a `Request`, or a URL string (absolute or path). |
14
+ | `env` | `TEnv` | Environment bindings forwarded to matching and middleware (surfaced as `ctx.env`). |
15
+
16
+ ### Context — response-handler `ctx` (what your code receives)
17
+
18
+ The lightweight context a RESPONSE-route handler reads (mirrors the production `handleResponseRoute` shape). Notable fields:
19
+
20
+ | Field | Type | Meaning |
21
+ | --------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
22
+ | `request` | `Request` | The dispatched request. |
23
+ | `params` | `Record<string, string>` | URL params from the matched route. |
24
+ | `env` | `TEnv` | Bindings from `opts.env`. |
25
+ | `searchParams` | `URLSearchParams` | Query params with internal `_rsc*` params stripped. |
26
+ | `url` | `URL` | Cleaned request URL (internal `_rsc*` params removed). |
27
+ | `pathname` | `string` | Matched pathname. |
28
+ | `reverse` | `ReverseFunction` | URL-from-name. Map-only (NO auto-fill from current params), matching the production response-route handler. |
29
+ | `get` | fn | Read context vars set by prior middleware. |
30
+ | `header(name, value)` | fn | Set a response header; surfaces on the returned `Response`. |
31
+ | `waitUntil` | fn | Register a deferred task (no-op fidelity in tests). |
32
+
33
+ ### Returns — `dispatch(router, opts) -> Promise<Response>`
34
+
35
+ ```ts
36
+ function dispatch<TEnv = any>(
37
+ router: Rango<TEnv, any>,
38
+ opts: DispatchOptions<TEnv>,
39
+ ): Promise<Response>;
40
+ ```
41
+
42
+ A real `Response`: response-route body, a 308 redirect (`Location`), a 404, or a middleware short-circuit. A `path.json` handler that returns a bare value is serialized verbatim (no envelope); a returned `Response` passes through unchanged; cookies and `ctx.header(...)` surface on the `Response`. `dispatch` accepts your public router type directly (no cast).
43
+
44
+ ## Recipe
45
+
46
+ ```ts
47
+ import { describe, it, expect } from "vitest";
48
+ import { dispatch } from "@rangojs/router/testing";
49
+ import { createRouter } from "@rangojs/router";
50
+ import { apiPatterns } from "../src/api/urls"; // path.json(...) routes, no Prerender
51
+
52
+ const router = createRouter().routes(apiPatterns);
53
+
54
+ describe("api routes via dispatch", () => {
55
+ it("serializes a JSON response route as the bare handler value", async () => {
56
+ const res = await dispatch(router, { request: "/health" });
57
+ expect(res.status).toBe(200);
58
+ expect(res.headers.get("content-type")).toBe(
59
+ "application/json;charset=utf-8",
60
+ );
61
+ expect(await res.json()).toEqual({ status: "ok" });
62
+ });
63
+
64
+ it("maps a thrown RouterError to its status + RFC 9457 problem+json", async () => {
65
+ const res = await dispatch(router, { request: "/products/999" }); // handler throws RouterError 404
66
+ expect(res.status).toBe(404);
67
+ expect(res.headers.get("content-type")).toBe(
68
+ "application/problem+json;charset=utf-8",
69
+ );
70
+ expect((await res.json()).code).toBe("NOT_FOUND"); // { title, status, detail, code }
71
+ });
72
+
73
+ it("returns 404 for an unmatched path", async () => {
74
+ expect((await dispatch(router, { request: "/nope" })).status).toBe(404);
75
+ });
76
+ });
77
+ ```
78
+
79
+ `dispatch` also covers trailing-slash/redirect targets (`findMatch`) — a redirected path returns a 308 with the `Location` (query preserved). Pass `env` via `{ env }`.
80
+
81
+ ## Caveats
82
+
83
+ - Hitting a COMPONENT (RSC) route throws a clear directive error: `dispatch` is for response routes + redirects + 404 + content negotiation, plus the global + route-level middleware guard stack on RESPONSE routes — it never renders React. Use Flight primitives or e2e to exercise component rendering.
84
+ - A COMPONENT route's guard stack cannot run here. Assert it at e2e, or extract the middleware fn and unit-test it with `runMiddleware` (see `./middleware.md`).
85
+ - JSON serialization is bare, applied in `response-route-handler.ts`: a `path.json` handler that returns a value is serialized verbatim (`JSON.stringify(value)`, status 200, `application/json`) — no envelope. Returning a `Response` (e.g. `Response.json(x)`) passes through unchanged. A thrown error yields an RFC 9457 problem+json body `{ title, status, detail, code }` (`application/problem+json`) with the error's status (`RouterError.status`, else 500, or the effective `ctx.setStatus()` override); `code` is the `RouterError.code`, else `"INTERNAL"`. The `type` member is omitted this phase. Assert the shape matching what your handler returns.
86
+ - Setup: needs the preset (alias + virtual stubs) or a Vite-RSC env (see `./setup.md`); a bare router import throws on Vite virtuals.
87
+ - A router using `Prerender()`/`createLoader()`/`Static()` now constructs in a bare test (each assigns a runtime fallback `$$id`). Importing the whole router _file_ may still need the plugin (its page modules pull app deps / `virtual:` modules) — build from a focused include (your API routes) for whole-router dispatch.
88
+ - A `_rsc_partial` request to a response route runs global middleware first (an auth gate can still 401/redirect), then returns `X-RSC-Reload` — route-level middleware is skipped, exactly like production.
89
+ - `dispatch` does NOT execute server actions (`?_rsc_action`), but it DOES run the global middleware chain on an action request — middleware can still 401/redirect it, and any 3xx redirect on a partial OR action request becomes a `204` + `X-RSC-Redirect` (fetch-safe interception), the raw `Location` dropped.
90
+
91
+ ## See also
92
+
93
+ - `/response-routes`, `/mime-routes` — the DSL this tests
94
+ - Siblings: `./middleware.md`, `./setup.md`, `./cache-prerender.md`
95
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "dispatch — request to Response" (the `rangoTestConfig` preset stubs `@vitejs/plugin-rsc/rsc`, so no per-file `vi.mock` is needed)
@@ -0,0 +1,84 @@
1
+ # Testing reverse/href and type-level contracts
2
+
3
+ **Layer:** unit (node) + typecheck · **Import:** `@rangojs/router/client` (useReverse), `@rangojs/router/testing` (assertGeneratedRoutesMatch) · **DSL it tests:** `reverse`/`href`/`useReverse` (see `/typesafety`, `/links`)
4
+
5
+ The reverse/href/params/env types are a real contract: a wrong route name, a missing param, or an unknown env binding should be a COMPILE error, not a runtime surprise. The type-test recipes have no runtime API — `tsc --noEmit` IS the assertion. `assertGeneratedRoutesMatch` is the one runtime helper here: it runs the router's real matching to expand lazy includes, then diffs the live `routeMap` against the generated named-routes map you seed.
6
+
7
+ ## API
8
+
9
+ ### Options — `assertGeneratedRoutesMatch(router, generatedMap?)`
10
+
11
+ | Field | Type | Meaning |
12
+ | -------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `router` | `{ routeMap; findMatch? }` | Your router (real impl). `routeMap` is the live name→pattern map; `findMatch` (when present) is called to force-expand lazy `include()`d routes. |
14
+ | `generatedMap` | `Record<string, unknown>` (optional) | The imported `*.named-routes.gen.ts` map (name→pattern, or `{ path }` objects). Omit to diff against the global route map (`getGlobalRouteMap()`) instead. |
15
+
16
+ ### Context — `GeneratedRoutesDiff` (what `diffGeneratedRoutes` returns)
17
+
18
+ | Field | Type | Meaning |
19
+ | ---------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `missing` | `string[]` | Names in the generated map but absent at runtime (stale generated entry). |
21
+ | `extra` | `string[]` | Names at runtime but absent from the generated map (ungenerated route). Auto-generated internal names (`$path_*`/`$prefix_*`) are excluded. |
22
+ | `mismatch` | `[name, generated, runtime][]` | Names in both whose patterns differ. |
23
+ | `ok` | `boolean` | True when `missing`, `extra`, and `mismatch` are all empty. |
24
+
25
+ ### Returns — `assertGeneratedRoutesMatch`
26
+
27
+ `void` on match. On drift, throws an `Error` listing every missing, extra, and mismatched route plus a "regenerate the `*.named-routes.gen.ts` file" hint. (`diffGeneratedRoutes` returns the `GeneratedRoutesDiff` above without throwing.)
28
+
29
+ ## Recipe
30
+
31
+ ```ts
32
+ // 1. Negative assertions inline with @ts-expect-error — the directive ERRORS if
33
+ // the line below it ever starts compiling (i.e. if the type guard regresses).
34
+ // Validated by `tsc --noEmit`; a runtime test cannot assert this.
35
+ import { useReverse } from "@rangojs/router/client";
36
+
37
+ const reverse = useReverse({ post: "/blog/:slug" });
38
+ reverse("post", { slug: "hi" }); // ok
39
+ // @ts-expect-error - missing required :slug param
40
+ reverse("post", {});
41
+ // @ts-expect-error - "comment" is not a route in this map
42
+ reverse("comment", { id: "1" });
43
+ ```
44
+
45
+ ```ts
46
+ // 2. Positive assertions with vitest's expectTypeOf — pin an INFERRED type
47
+ // (loader return, parsed search schema, RouteParams) inside a normal *.test.ts.
48
+ import { expectTypeOf } from "vitest";
49
+ import type { RouteParams } from "@rangojs/router";
50
+
51
+ // RouteParams takes a route NAME and a route map (defaulting to the global map).
52
+ // Pass an explicit map to keep the type test self-contained.
53
+ expectTypeOf<
54
+ RouteParams<"blogPost", { blogPost: "/blog/:slug" }>
55
+ >().toEqualTypeOf<{ slug: string }>();
56
+ ```
57
+
58
+ ```ts
59
+ // 3. assertGeneratedRoutesMatch — a one-liner whole-app drift test. Real
60
+ // matching expands lazy include()d routes before the diff.
61
+ import { it } from "vitest";
62
+ import { assertGeneratedRoutesMatch } from "@rangojs/router/testing";
63
+ import { router } from "../src/router";
64
+ import generated from "../src/router.named-routes.gen";
65
+
66
+ it("generated named-routes map is in sync with the router", () => {
67
+ assertGeneratedRoutesMatch(router, generated);
68
+ });
69
+ ```
70
+
71
+ For a large type-only suite, collect recipe-1/2 assertions in `*.test-d.ts` files and add a `tsconfig.types.json` that `extends` your base config and `include`s only those files, then run `tsc -p tsconfig.types.json --noEmit` in CI. This is how the repo pins its own augmentation contracts. Recipe 1 is enough for most apps; reach for the dedicated tsconfig only when inline assertions clutter runtime tests.
72
+
73
+ ## Caveats
74
+
75
+ - Type tests run at TYPECHECK time (`tsc --noEmit`), NOT in the vitest runner. They are their own layer — wire them into CI as a real step (`pnpm run typecheck`). A type test nobody runs is just a comment.
76
+ - `@ts-expect-error` ERRORS if the line below it ever starts compiling, so a regressed guard fails the typecheck. A runtime test cannot assert "this should not type-check".
77
+ - `assertGeneratedRoutesMatch` force-expands lazy `include()`d routes (calls `findMatch` on a concrete path derived from each generated pattern) before diffing — otherwise every included route reads as a false `missing`. This makes the whole-app drift check work in a plain unit test. Routers without `findMatch` (a bare `{ routeMap }`) are left as-is.
78
+ - MULTI-APP route-map isolation. `href()`/`reverse()` typing is GLOBAL — each app's generated file augments the one `Rango.GeneratedRouteMap` interface. A `renderRoute` suite that imports a client component from app B (which calls `href("/b-route")`) won't typecheck if the same tsconfig program also carries app A's augmentation: A's route union rejects B's name. `renderRoute` is app-agnostic at RUNTIME; the collision is purely the global `href` typing. Keep a `renderRoute` suite single-app, or give each app its OWN tsconfig program (see `/typesafety`); a quick sidestep is to probe `useMount`/`useHref` inline instead of importing the cross-app component.
79
+
80
+ ## See also
81
+
82
+ - `/typesafety`, `/links` — the DSL this tests
83
+ - Siblings: `./client-components.md`, `./loader.md`
84
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Type-level tests — make misuse fail to compile"