@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125

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 (260) hide show
  1. package/dist/bin/rango.js +10 -6
  2. package/dist/testing/vitest.js +82 -0
  3. package/dist/vite/index.js +55 -48
  4. package/package.json +61 -21
  5. package/skills/caching/SKILL.md +2 -1
  6. package/skills/hooks/SKILL.md +40 -29
  7. package/skills/host-router/SKILL.md +16 -2
  8. package/skills/intercept/SKILL.md +4 -2
  9. package/skills/layout/SKILL.md +11 -6
  10. package/skills/loader/SKILL.md +6 -2
  11. package/skills/middleware/SKILL.md +4 -2
  12. package/skills/migrate-nextjs/SKILL.md +3 -1
  13. package/skills/parallel/SKILL.md +9 -4
  14. package/skills/rango/SKILL.md +12 -0
  15. package/skills/route/SKILL.md +10 -2
  16. package/skills/testing/SKILL.md +129 -0
  17. package/skills/testing/bindings.md +89 -0
  18. package/skills/testing/cache-prerender.md +98 -0
  19. package/skills/testing/client-components.md +122 -0
  20. package/skills/testing/e2e-parity.md +125 -0
  21. package/skills/testing/flight.md +89 -0
  22. package/skills/testing/handles.md +129 -0
  23. package/skills/testing/loader.md +128 -0
  24. package/skills/testing/middleware.md +99 -0
  25. package/skills/testing/render-handler.md +118 -0
  26. package/skills/testing/response-routes.md +95 -0
  27. package/skills/testing/reverse-and-types.md +84 -0
  28. package/skills/testing/server-actions.md +107 -0
  29. package/skills/testing/server-tree.md +128 -0
  30. package/skills/testing/setup.md +120 -0
  31. package/src/__internal.ts +0 -65
  32. package/src/browser/action-coordinator.ts +1 -1
  33. package/src/browser/action-fence.ts +47 -0
  34. package/src/browser/cookie-name.ts +140 -0
  35. package/src/browser/event-controller.ts +1 -83
  36. package/src/browser/invalidate-client-cache.ts +52 -0
  37. package/src/browser/navigation-bridge.ts +14 -1
  38. package/src/browser/navigation-client.ts +14 -1
  39. package/src/browser/navigation-store-handle.ts +38 -0
  40. package/src/browser/navigation-store.ts +26 -51
  41. package/src/browser/navigation-transaction.ts +0 -32
  42. package/src/browser/partial-update.ts +1 -83
  43. package/src/browser/prefetch/cache.ts +6 -45
  44. package/src/browser/prefetch/fetch.ts +7 -0
  45. package/src/browser/prefetch/queue.ts +6 -3
  46. package/src/browser/rango-state.ts +157 -99
  47. package/src/browser/react/Link.tsx +0 -2
  48. package/src/browser/react/NavigationProvider.tsx +2 -1
  49. package/src/browser/react/ScrollRestoration.tsx +10 -6
  50. package/src/browser/react/filter-segment-order.ts +0 -2
  51. package/src/browser/react/index.ts +0 -51
  52. package/src/browser/react/location-state-shared.ts +0 -13
  53. package/src/browser/react/location-state.ts +0 -1
  54. package/src/browser/react/use-action.ts +6 -15
  55. package/src/browser/react/use-handle.ts +0 -5
  56. package/src/browser/react/use-link-status.ts +0 -4
  57. package/src/browser/react/use-navigation.ts +0 -3
  58. package/src/browser/react/use-params.ts +0 -2
  59. package/src/browser/react/use-search-params.ts +0 -5
  60. package/src/browser/react/use-segments.ts +0 -13
  61. package/src/browser/rsc-router.tsx +12 -4
  62. package/src/browser/server-action-bridge.ts +77 -15
  63. package/src/browser/types.ts +7 -2
  64. package/src/browser/validate-redirect-origin.ts +4 -5
  65. package/src/build/route-trie.ts +3 -0
  66. package/src/build/route-types/param-extraction.ts +6 -3
  67. package/src/build/route-types/router-processing.ts +0 -8
  68. package/src/cache/cache-policy.ts +0 -54
  69. package/src/cache/cache-runtime.ts +27 -24
  70. package/src/cache/cache-scope.ts +0 -27
  71. package/src/cache/cache-tag.ts +0 -37
  72. package/src/cache/cf/cf-cache-store.ts +94 -46
  73. package/src/cache/cf/index.ts +0 -24
  74. package/src/cache/document-cache.ts +11 -36
  75. package/src/cache/handle-snapshot.ts +0 -40
  76. package/src/cache/index.ts +0 -27
  77. package/src/cache/memory-segment-store.ts +2 -48
  78. package/src/cache/profile-registry.ts +7 -3
  79. package/src/cache/read-through-swr.ts +41 -11
  80. package/src/cache/segment-codec.ts +0 -16
  81. package/src/cache/types.ts +0 -98
  82. package/src/client.rsc.tsx +1 -22
  83. package/src/client.tsx +14 -38
  84. package/src/component-utils.ts +19 -0
  85. package/src/deps/ssr.ts +0 -1
  86. package/src/handle.ts +28 -18
  87. package/src/handles/MetaTags.tsx +0 -14
  88. package/src/handles/meta.ts +0 -39
  89. package/src/host/cookie-handler.ts +0 -36
  90. package/src/host/errors.ts +0 -24
  91. package/src/host/index.ts +6 -0
  92. package/src/host/pattern-matcher.ts +7 -50
  93. package/src/host/router.ts +1 -65
  94. package/src/host/testing.ts +40 -27
  95. package/src/host/types.ts +6 -2
  96. package/src/href-client.ts +0 -4
  97. package/src/index.rsc.ts +42 -3
  98. package/src/index.ts +31 -1
  99. package/src/internal-debug.ts +2 -4
  100. package/src/loader.rsc.ts +19 -9
  101. package/src/loader.ts +12 -4
  102. package/src/network-error-thrower.tsx +1 -6
  103. package/src/outlet-provider.tsx +1 -5
  104. package/src/prerender/param-hash.ts +10 -11
  105. package/src/prerender/store.ts +23 -30
  106. package/src/prerender.ts +58 -3
  107. package/src/root-error-boundary.tsx +1 -19
  108. package/src/route-content-wrapper.tsx +1 -44
  109. package/src/route-definition/dsl-helpers.ts +7 -19
  110. package/src/route-definition/helpers-types.ts +3 -3
  111. package/src/route-definition/redirect.ts +11 -1
  112. package/src/route-map-builder.ts +0 -16
  113. package/src/router/basename.ts +14 -0
  114. package/src/router/content-negotiation.ts +0 -13
  115. package/src/router/error-handling.ts +12 -16
  116. package/src/router/find-match.ts +4 -30
  117. package/src/router/intercept-resolution.ts +10 -1
  118. package/src/router/lazy-includes.ts +1 -57
  119. package/src/router/loader-resolution.ts +3 -2
  120. package/src/router/logging.ts +0 -6
  121. package/src/router/manifest.ts +1 -25
  122. package/src/router/match-api.ts +0 -20
  123. package/src/router/match-context.ts +0 -22
  124. package/src/router/match-handlers.ts +57 -58
  125. package/src/router/match-middleware/background-revalidation.ts +0 -7
  126. package/src/router/match-middleware/cache-lookup.ts +1 -54
  127. package/src/router/match-middleware/cache-store.ts +0 -31
  128. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  129. package/src/router/match-middleware/segment-resolution.ts +0 -21
  130. package/src/router/match-pipelines.ts +1 -42
  131. package/src/router/match-result.ts +1 -52
  132. package/src/router/metrics.ts +0 -34
  133. package/src/router/middleware-cookies.ts +0 -13
  134. package/src/router/middleware-types.ts +0 -115
  135. package/src/router/middleware.ts +7 -30
  136. package/src/router/navigation-snapshot.ts +0 -51
  137. package/src/router/params-util.ts +23 -0
  138. package/src/router/pattern-matching.ts +1 -33
  139. package/src/router/prerender-match.ts +33 -45
  140. package/src/router/request-classification.ts +1 -38
  141. package/src/router/revalidation.ts +5 -58
  142. package/src/router/router-context.ts +0 -26
  143. package/src/router/router-interfaces.ts +7 -0
  144. package/src/router/router-options.ts +30 -0
  145. package/src/router/segment-resolution/fresh.ts +25 -57
  146. package/src/router/segment-resolution/helpers.ts +34 -0
  147. package/src/router/segment-resolution/loader-cache.ts +10 -13
  148. package/src/router/segment-resolution/revalidation.ts +5 -42
  149. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  150. package/src/router/segment-resolution.ts +4 -1
  151. package/src/router/state-cookie-name.ts +33 -0
  152. package/src/router/telemetry-otel.ts +0 -20
  153. package/src/router/telemetry.ts +96 -19
  154. package/src/router/timeout.ts +0 -20
  155. package/src/router/trie-matching.ts +63 -40
  156. package/src/router/types.ts +1 -63
  157. package/src/router/url-params.ts +0 -5
  158. package/src/router.ts +40 -9
  159. package/src/rsc/handler.ts +14 -2
  160. package/src/rsc/helpers.ts +34 -0
  161. package/src/rsc/origin-guard.ts +0 -12
  162. package/src/rsc/progressive-enhancement.ts +4 -1
  163. package/src/rsc/rsc-rendering.ts +4 -7
  164. package/src/rsc/runtime-warnings.ts +14 -0
  165. package/src/rsc/server-action.ts +30 -28
  166. package/src/rsc/types.ts +2 -1
  167. package/src/runtime-env.ts +18 -0
  168. package/src/search-params.ts +0 -16
  169. package/src/segment-loader-promise.ts +14 -2
  170. package/src/segment-system.tsx +79 -88
  171. package/src/server/cookie-store.ts +52 -1
  172. package/src/server/handle-store.ts +7 -24
  173. package/src/server/loader-registry.ts +5 -24
  174. package/src/server/request-context.ts +74 -77
  175. package/src/ssr/index.tsx +14 -14
  176. package/src/static-handler.ts +10 -13
  177. package/src/testing/cache-status.ts +119 -0
  178. package/src/testing/collect-handle.ts +40 -0
  179. package/src/testing/dispatch.ts +581 -0
  180. package/src/testing/dom.entry.ts +22 -0
  181. package/src/testing/e2e/fixture.ts +188 -0
  182. package/src/testing/e2e/index.ts +127 -0
  183. package/src/testing/e2e/matchers.ts +35 -0
  184. package/src/testing/e2e/page-helpers.ts +272 -0
  185. package/src/testing/e2e/parity.ts +387 -0
  186. package/src/testing/e2e/server.ts +195 -0
  187. package/src/testing/flight-matchers.ts +97 -0
  188. package/src/testing/flight-normalize.ts +11 -0
  189. package/src/testing/flight-runtime.d.ts +57 -0
  190. package/src/testing/flight-tree.ts +682 -0
  191. package/src/testing/flight.entry.ts +52 -0
  192. package/src/testing/flight.ts +186 -0
  193. package/src/testing/generated-routes.ts +183 -0
  194. package/src/testing/index.ts +98 -0
  195. package/src/testing/internal/context.ts +348 -0
  196. package/src/testing/internal/flight-client-globals.ts +30 -0
  197. package/src/testing/internal/seed-vars.ts +54 -0
  198. package/src/testing/render-handler.ts +311 -0
  199. package/src/testing/render-route.tsx +504 -0
  200. package/src/testing/run-loader.ts +378 -0
  201. package/src/testing/run-middleware.ts +205 -0
  202. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  203. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  204. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  205. package/src/testing/vitest-stubs/version.ts +5 -0
  206. package/src/testing/vitest.ts +305 -0
  207. package/src/theme/ThemeProvider.tsx +0 -52
  208. package/src/theme/ThemeScript.tsx +0 -6
  209. package/src/theme/constants.ts +0 -12
  210. package/src/theme/index.ts +0 -7
  211. package/src/theme/theme-context.ts +1 -5
  212. package/src/theme/theme-script.ts +0 -14
  213. package/src/theme/use-theme.ts +0 -3
  214. package/src/types/boundaries.ts +0 -35
  215. package/src/types/error-types.ts +25 -89
  216. package/src/types/global-namespace.ts +15 -15
  217. package/src/types/handler-context.ts +16 -13
  218. package/src/types/index.ts +0 -10
  219. package/src/types/request-scope.ts +0 -19
  220. package/src/types/route-config.ts +6 -50
  221. package/src/types/route-entry.ts +0 -6
  222. package/src/types/segments.ts +0 -13
  223. package/src/urls/include-helper.ts +0 -4
  224. package/src/urls/index.ts +0 -6
  225. package/src/urls/path-helper-types.ts +2 -2
  226. package/src/urls/path-helper.ts +0 -54
  227. package/src/urls/urls-function.ts +0 -13
  228. package/src/use-loader.tsx +0 -186
  229. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  230. package/src/vite/discovery/discover-routers.ts +6 -7
  231. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  232. package/src/vite/plugin-types.ts +3 -1
  233. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  234. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  235. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  236. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  237. package/src/vite/plugins/expose-action-id.ts +2 -73
  238. package/src/vite/plugins/expose-id-utils.ts +0 -55
  239. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  240. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  241. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  242. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  243. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  244. package/src/vite/plugins/performance-tracks.ts +0 -3
  245. package/src/vite/plugins/use-cache-transform.ts +0 -36
  246. package/src/vite/plugins/version-injector.ts +0 -20
  247. package/src/vite/plugins/version-plugin.ts +1 -49
  248. package/src/vite/plugins/virtual-entries.ts +0 -15
  249. package/src/vite/rango.ts +1 -108
  250. package/src/vite/router-discovery.ts +2 -1
  251. package/src/vite/utils/ast-handler-extract.ts +0 -16
  252. package/src/vite/utils/bundle-analysis.ts +6 -13
  253. package/src/vite/utils/client-chunks.ts +0 -6
  254. package/src/vite/utils/forward-user-plugins.ts +0 -22
  255. package/src/vite/utils/manifest-utils.ts +0 -4
  256. package/src/vite/utils/package-resolution.ts +1 -73
  257. package/src/vite/utils/prerender-utils.ts +0 -35
  258. package/src/vite/utils/shared-utils.ts +3 -35
  259. package/src/browser/react/use-client-cache.ts +0 -58
  260. package/src/browser/shallow.ts +0 -40
@@ -0,0 +1,128 @@
1
+ # Inspecting the rendered tree — renderServerTree, findClientBoundaries, findElements
2
+
3
+ **Layer:** RSC unit (react-server project) · **Import:** `@rangojs/router/testing/flight` · **DSL it tests:** client islands across the boundary + server-rendered host content (see `/route`)
4
+
5
+ `renderServerTree` serializes the real Flight (identical bytes to `renderToFlightString`) and then deserializes it back to an inspectable React element tree you traverse — that serialize/deserialize round-trip is REAL; what you SEED is the element you render plus the request context (`request`/`headers`/`params`/`vars`/`env`). The win over the wire string: a client boundary's props come back as real JS values (a `Date` is a `Date`, not the opaque `$D...` encoding) and you can confirm a `"use client"` component actually crossed the boundary (an `I` row) instead of being inlined. There is NO hydration and NO interaction — boundaries are inert placeholders carrying props.
6
+
7
+ ## API
8
+
9
+ ### Options — `RenderServerTreeOptions` (extends `RenderToFlightStringOptions`)
10
+
11
+ | Field | Type | Meaning |
12
+ | ------------------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `request` | `Request \| string` | The request the render runs under (absolute URL, path, or a `Request`). Defaults to `http://localhost/`. A server component reading `getRequestContext()` sees this url/cookies. A passed `Request`'s headers win; `headers` is then ignored. |
14
+ | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) visible to the server tree, when `request` is a string. |
15
+ | `env` | `unknown` | Env / bindings exposed as `ctx.env`. Defaults to `{}`. |
16
+ | `params` | `Record<string, string>` | Route params exposed via `ctx.params` and loader contexts. |
17
+ | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
18
+ | `vars` | `VarsInit` | Context variables visible via `ctx.get(...)`, as a prior middleware would have set them. Object form (`{ user }`) or `[key, value]` tuples. |
19
+ | `clientComponents` | `Record<string, unknown>` | The `"use client"` components reachable from the tree, keyed by the boundary name to register each as a client reference (in place) so it serializes as an `I` row. Omit when `rangoUseClientTransform()` auto-discovers them, or for pure server-only trees. First-wins per worker; already-registered references are left untouched. |
20
+
21
+ ### Context — what your code receives
22
+
23
+ A server component rendered here runs under a real request context: `getRequestContext()` resolves, `ctx.params`/`ctx.routeName`/`ctx.env` reflect the options, `ctx.get(MyVar)` reads a seeded `var`, and cookies come off the request. Same seeding as the handler-test primitives — you render an **element** you build (`<Page />`); to run a route **handler** `(ctx) => rsc` use `renderHandler` (see `./render-handler.md`).
24
+
25
+ ### Returns — `RenderServerTreeResult`
26
+
27
+ ```ts
28
+ renderServerTree(element, opts?): Promise<{ flight: string; tree: unknown }>
29
+ ```
30
+
31
+ | Field | Type | Meaning |
32
+ | -------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
+ | `flight` | `string` | The raw Flight wire string (so `toMatchFlight` assertions still apply). |
34
+ | `tree` | `unknown` | The deserialized React element tree. Server elements are plain React elements; each client boundary is an inert placeholder whose `props` are the real deserialized JS values that crossed the boundary. |
35
+
36
+ #### `findClientBoundaries(tree, selector?) -> ClientBoundary[]`
37
+
38
+ Every client boundary in document order; always an array (no throw on zero/many — destructure `const [tag] = ...` and assert `.length` when the count matters; no match yields `[]`). `selector` is a STRING (match by export name) or a `BoundarySelector` object, criteria AND-ed.
39
+
40
+ | `BoundarySelector` | Type | Meaning |
41
+ | ------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------ |
42
+ | `name` | `string` | Match the boundary's export name (same as a bare string). |
43
+ | `testId` | `string` | Match `props["data-testid"]` exactly (a `data-testid` you passed AS A PROP to the island). |
44
+ | `props` | `Record<string, unknown>` | Subset deep-equal match (Date/Map/Set/array/nested-object aware); unlisted props ignored. |
45
+ | `where` | `(boundary: ClientBoundary) => boolean` | Arbitrary predicate. |
46
+
47
+ `ClientBoundary` = `{ id, name, props (excludes children), children, element }`.
48
+
49
+ #### `findElements(tree, selector?) -> FoundElement[]`
50
+
51
+ Every SERVER/HOST element a server component produced (`<article>`, `<h2>`), in document order; always an array. `selector` is a host TAG string (`"h2"`) or an `ElementSelector` object, criteria AND-ed.
52
+
53
+ | `ElementSelector` | Type | Meaning |
54
+ | ----------------- | ------------------------------------ | ---------------------------------------------------------------------------------- |
55
+ | `tag` | `string` | Match the host tag name (`"article"`, `"h2"`). |
56
+ | `testId` | `string` | Match `props["data-testid"]` exactly (on a host element). |
57
+ | `props` | `Record<string, unknown>` | Subset deep-equal match (Date/Map/Set/array/nested aware). |
58
+ | `text` | `string \| RegExp` | Match the element's text content (substring for a string, `.test()` for a RegExp). |
59
+ | `where` | `(element: FoundElement) => boolean` | Arbitrary predicate. |
60
+
61
+ `FoundElement` = `{ tag, props (excludes children), children, text, element }`.
62
+
63
+ #### `textContent(node) -> string`
64
+
65
+ Concatenates every string/number leaf of a node's subtree in document order — the clean way to assert rendered text, instead of `JSON.stringify(tree).toContain(...)`.
66
+
67
+ ## Recipe
68
+
69
+ ```tsx
70
+ import { it, expect } from "vitest";
71
+ import {
72
+ renderServerTree,
73
+ findClientBoundaries,
74
+ findElements,
75
+ textContent,
76
+ } from "@rangojs/router/testing/flight";
77
+ import { PriceTag } from "./PriceTag.js"; // a "use client" component (any filename)
78
+
79
+ async function ProductPanel({ amount, asOf }: { amount: number; asOf: Date }) {
80
+ await Promise.resolve();
81
+ return (
82
+ <article>
83
+ <h2>Wine</h2>
84
+ <PriceTag amount={amount} currency="USD" asOf={asOf} />
85
+ </article>
86
+ );
87
+ }
88
+
89
+ it("client props survive the serialize -> deserialize round trip", async () => {
90
+ const { flight, tree } = await renderServerTree(
91
+ <ProductPanel amount={19.5} asOf={new Date("2026-01-02T00:00:00Z")} />,
92
+ // Omit clientComponents when rangoUseClientTransform() is wired (see ./setup.md);
93
+ // otherwise register islands explicitly:
94
+ { clientComponents: { PriceTag } },
95
+ );
96
+ expect(flight).toMatchFlight("PriceTag"); // wire assertions still work
97
+
98
+ const [tag] = findClientBoundaries(tree, "PriceTag");
99
+ expect(tag.props.amount).toBe(19.5); // a real number
100
+ expect(tag.props.asOf).toBeInstanceOf(Date); // a real Date, not "$D..."
101
+ });
102
+
103
+ it("asserts the server-rendered host content", async () => {
104
+ const { tree } = await renderServerTree(
105
+ <ProductPanel amount={19.5} asOf={new Date("2026-01-02T00:00:00Z")} />,
106
+ { clientComponents: { PriceTag } },
107
+ );
108
+ const [h2] = findElements(tree, "h2");
109
+ expect(h2.text).toBe("Wine");
110
+ expect(textContent(tree)).toContain("Wine"); // instead of JSON.stringify(tree)
111
+ });
112
+ ```
113
+
114
+ ## Caveats
115
+
116
+ - This renders an ELEMENT you build (`<Page />`). To test a route HANDLER (a `(ctx) => rsc` function registered via `path(...)`), use `renderHandler` (see [`./render-handler.md`](./render-handler.md)) — handlers have their own util. Do NOT wrap a handler in `createElement` and render it here: a handler is not a component, so React would invoke it with `props` as its argument instead of the real `HandlerContext`, and the seeded `params`/`vars` plus `ctx.use`/`ctx.reverse`/`ctx.get`/`cookies()` would all be absent.
117
+ - Island auto-discovery from the server tree's imports needs `rangoUseClientTransform()` in the rsc project (see `./setup.md`). Without it a plainly-imported island is just a function the serializer renders server-side — register islands explicitly via `{ clientComponents: { PriceTag } }`.
118
+ - Same alias requirement as `./flight.md`: a rendered component (or handler) that reads `getRequestContext()`/`cookies()` from the `@rangojs/router` barrel needs the `index.rsc.ts` alias (see `./setup.md`), or it hits the throwing out-of-react-server stub.
119
+ - A client boundary's props come back as REAL JS values after deserialization (a `Date` is a `Date`, not a `$D...` encoding) — but there is NO hydration and NO interaction; boundaries are inert placeholders carrying props.
120
+ - Server COMPONENTS do not survive Flight as identities (they are executed during serialization), so `findElements` matches the host elements they PRODUCED, not the component function. Client islands keep identity — use `findClientBoundaries` for those.
121
+ - `findClientBoundaries` finds islands (`I` rows); `findElements` finds host elements. A `testId` on an island matches with `findClientBoundaries`; a `testId` on a host element matches with `findElements`. Use `textContent(node)` in place of `JSON.stringify(tree).toContain`.
122
+ - A true interactive, clickable DOM `renderServer` is intentionally NOT shipped: in-process happy-dom hydration re-tests React more than your app and misses server/client divergence (the only hydration bug worth a dedicated test, which needs a real browser). Test interaction at e2e.
123
+
124
+ ## See also
125
+
126
+ - `/route` — the DSL this tests
127
+ - Siblings: `./flight.md`, `./render-handler.md`, `./setup.md`
128
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "renderServerTree — serialize then deserialize to an inspectable tree" (and the "findElements / textContent" subsection)
@@ -0,0 +1,120 @@
1
+ # Testing setup — the two vitest projects
2
+
3
+ **Layer:** cross-cutting (vitest config) · **Import:** `@rangojs/router/testing/vitest`
4
+
5
+ Real machinery: Vite transpiles `@rangojs/router`'s shipped TS source and resolves the bare specifier to its react-server impls so your app's router / loaders / middleware import in a bare Vitest process. You SEED nothing here — this file only wires the two projects every other recipe builds on. The node/DOM project keeps React on its CLIENT build; the Flight project flips to the `react-server` condition.
6
+
7
+ ## API
8
+
9
+ ### Options — `RangoTestAliasOptions`
10
+
11
+ | Field | Type | Meaning |
12
+ | -------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `preset` | `"node" \| "cloudflare"` | Deployment preset, matching `rango({ preset })` in the Vite plugin. `"cloudflare"` additionally stubs the `cloudflare:workers` / `cloudflare:email` runtime virtuals a CF route tree imports. A string (not a boolean) so more presets can be added without an API change. Default `"node"`. |
14
+
15
+ ### Functions
16
+
17
+ | Function | Returns | Use |
18
+ | --------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `rangoTestConfig(opts?)` | `{ alias, server: { deps: { inline } } }` | Recommended. Spread into the node/DOM project's `test` block. Bundles the resolve aliases AND `server.deps.inline`. |
20
+ | `rangoTestAliases(opts?)` | `TestAlias[]` (`{ find, replacement }[]`) | Lower-level. The bare `@rangojs/router` -> `index.rsc.ts` alias plus the `:version` / `@vitejs/plugin-rsc/rsc` stubs (and CF stubs under `preset:"cloudflare"`). Used in the rsc project's `resolve.alias`. |
21
+ | `rangoUseClientTransform()` | a Vite plugin (`{ name, transform }`) | Add to the rsc project `plugins`. Applies the `"use client"` transform so `renderServerTree` auto-discovers client islands from the server tree's imports. |
22
+
23
+ ### Returns — `RangoTestConfig` (from `rangoTestConfig`)
24
+
25
+ ```ts
26
+ interface RangoTestConfig {
27
+ alias: TestAlias[]; // -> test.alias
28
+ server: { deps: { inline: RegExp[] } }; // [/@rangojs[/\\]router/] -> test.server.deps.inline
29
+ }
30
+ ```
31
+
32
+ ## Recipe
33
+
34
+ ```ts
35
+ // vitest.config.ts — the node + DOM project (keeps React on its CLIENT build)
36
+ import { defineConfig } from "vitest/config";
37
+ import { rangoTestConfig } from "@rangojs/router/testing/vitest";
38
+
39
+ export default defineConfig({
40
+ test: {
41
+ globals: true,
42
+ include: ["test/**/*.test.{ts,tsx}"],
43
+ environment: "node", // renderRoute tests add a `// @vitest-environment happy-dom` pragma
44
+ // `preset: "cloudflare"` also stubs cloudflare:workers / cloudflare:email (default "node").
45
+ ...rangoTestConfig({ preset: "cloudflare" }),
46
+ },
47
+ });
48
+ ```
49
+
50
+ ```ts
51
+ // vitest.rsc.config.ts — the Flight project (react-server condition)
52
+ import { defineConfig } from "vitest/config";
53
+ import {
54
+ rangoTestAliases,
55
+ rangoUseClientTransform,
56
+ } from "@rangojs/router/testing/vitest";
57
+
58
+ // Production React in this process AND any forked worker (forks inherit env).
59
+ process.env.NODE_ENV = "production";
60
+
61
+ export default defineConfig({
62
+ plugins: [rangoUseClientTransform()],
63
+ resolve: {
64
+ conditions: ["react-server"],
65
+ alias: rangoTestAliases({ preset: "cloudflare" }), // or { preset: "node" }
66
+ },
67
+ test: {
68
+ globals: true,
69
+ include: ["**/*.rsc-test.{ts,tsx}"],
70
+ pool: "forks",
71
+ execArgv: ["--conditions=react-server"], // or React throws "react-server condition must be enabled"
72
+ },
73
+ });
74
+ ```
75
+
76
+ ```ts
77
+ // example.test.ts — one test in the node/DOM project, importing real app code
78
+ import { describe, it, expect } from "vitest";
79
+ import { dispatch } from "@rangojs/router/testing";
80
+ import { createRouter } from "@rangojs/router";
81
+ import { apiPatterns } from "../src/api/urls"; // path.json(...) routes only, no Prerender()
82
+
83
+ const router = createRouter().routes(apiPatterns);
84
+
85
+ describe("api", () => {
86
+ it("serializes a JSON response route", async () => {
87
+ const res = await dispatch(router, { request: "/health" });
88
+ expect(res.status).toBe(200);
89
+ expect(await res.json()).toEqual({ status: "ok" });
90
+ });
91
+ });
92
+ ```
93
+
94
+ Scripts:
95
+
96
+ ```jsonc
97
+ {
98
+ "scripts": {
99
+ "test:unit": "vitest run",
100
+ "test:unit:rsc": "vitest run --config vitest.rsc.config.ts",
101
+ },
102
+ }
103
+ ```
104
+
105
+ ## Caveats
106
+
107
+ - Node >= 23 requires `rangoTestConfig()`, not bare `rangoTestAliases()`. `@rangojs/router` is consumed as SOURCE (exports -> `./src/*.ts`), and Node >= 23 refuses to type-strip `.ts` under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`). `rangoTestConfig` ships as compiled JS AND adds `server.deps.inline: [/@rangojs[/\\]router/]` so Vite (not Node) transpiles rango source. With bare `rangoTestAliases` you must wire `deps.inline` yourself.
108
+ - Two separate projects. The node/DOM project keeps React on its CLIENT build; the Flight project uses the `react-server` condition in a separate `vitest.rsc.config.ts`. The main project must NOT set `react-server` — it flips React to the no-hooks server build and breaks every `renderRoute` / client test.
109
+ - The rsc project needs BOTH `resolve.conditions: ["react-server"]` AND the bare `@rangojs/router` -> `index.rsc.ts` alias from `rangoTestAliases({ preset })`. `resolve.conditions` alone is not reliably applied to bare-package export resolution; without the alias a handler/component reading `getRequestContext()` / `cookies()` resolves the throwing out-of-react-server stub (symptom: `renderHandler` returns `tree: undefined`).
110
+ - `NODE_ENV` must be `"production"` in the rsc project. Dev `NODE_ENV` crashes the bare worker (jsxDEV owner-stack machinery uninitialized) and emits volatile debug rows that defeat stable Flight snapshots.
111
+ - The forked rsc worker (`pool: "forks"`) must force the condition via `execArgv: ["--conditions=react-server"]`, or React throws "the react-server condition must be enabled".
112
+ - The `@rangojs/router:version` and `@vitejs/plugin-rsc/rsc` virtuals must be stubbed; the preset does it. A bare router import without stubbing throws.
113
+ - The rango fragment goes under `test` (`test.alias` + `test.server.deps.inline`, both returned by `rangoTestConfig`), NOT under top-level `resolve`.
114
+ - Wire `rangoUseClientTransform()` into the rsc project `plugins` so islands auto-discover from the server tree imports (see `./server-tree.md`); without it, register islands explicitly with `clientComponents`.
115
+
116
+ ## See also
117
+
118
+ - (cross-cutting)
119
+ - Siblings: `./flight.md`, `./server-tree.md`, `./render-handler.md`, `./response-routes.md`
120
+ - Long-form prose: [docs/testing.md](https://github.com/ivogt/vite-rsc/blob/main/packages/rangojs-router/docs/testing.md) — section "Setup" (and the subsections "Resolving @rangojs/router in a unit test — use the preset" and "Two vitest projects")
package/src/__internal.ts CHANGED
@@ -9,10 +9,6 @@
9
9
  * adding them to the public API.
10
10
  */
11
11
 
12
- // ============================================================================
13
- // Segment Resolution (Internal)
14
- // ============================================================================
15
-
16
12
  /**
17
13
  * @internal
18
14
  * Internal representation of a resolved route segment.
@@ -26,10 +22,6 @@ export type { ResolvedSegment, SegmentMetadata } from "./types.js";
26
22
  */
27
23
  export type { MatchResult, SlotState } from "./types.js";
28
24
 
29
- // ============================================================================
30
- // Intercept System (Internal)
31
- // ============================================================================
32
-
33
25
  /**
34
26
  * @internal
35
27
  * Context for intercept route selection.
@@ -40,10 +32,6 @@ export type {
40
32
  InterceptWhenFn,
41
33
  } from "./server/context.js";
42
34
 
43
- // ============================================================================
44
- // Browser State (Internal)
45
- // ============================================================================
46
-
47
35
  /**
48
36
  * @internal
49
37
  * RSC protocol payload structure.
@@ -68,10 +56,6 @@ export type {
68
56
  NavigationUpdate,
69
57
  } from "./browser/types.js";
70
58
 
71
- // ============================================================================
72
- // Handle System (Internal)
73
- // ============================================================================
74
-
75
59
  /**
76
60
  * @internal
77
61
  * Internal handle storage mechanism.
@@ -84,26 +68,17 @@ export type { HandleStore, HandleData } from "./server/handle-store.js";
84
68
  */
85
69
  export type { SegmentHandleData } from "./cache/types.js";
86
70
 
87
- // ============================================================================
88
- // Cache Internals
89
- // ============================================================================
90
-
91
71
  /**
92
72
  * @internal
93
73
  * Internal cache entry data structure.
94
74
  */
95
75
  export type {
96
76
  CachedEntryData,
97
- CachedEntryResult,
98
77
  CacheGetResult,
99
78
  SerializedSegmentData,
100
79
  CacheDefaults,
101
80
  } from "./cache/types.js";
102
81
 
103
- // ============================================================================
104
- // Router Context (Internal)
105
- // ============================================================================
106
-
107
82
  /**
108
83
  * @internal
109
84
  * Router context for AsyncLocalStorage.
@@ -114,10 +89,6 @@ export type {
114
89
  InterceptResult,
115
90
  } from "./router/router-context.js";
116
91
 
117
- // ============================================================================
118
- // Match Pipeline (Internal)
119
- // ============================================================================
120
-
121
92
  /**
122
93
  * @internal
123
94
  * Route match context during pipeline processing.
@@ -133,10 +104,6 @@ export type {
133
104
  */
134
105
  export type { RouteMatchResult } from "./router/pattern-matching.js";
135
106
 
136
- // ============================================================================
137
- // Server Context (Internal)
138
- // ============================================================================
139
-
140
107
  /**
141
108
  * @internal
142
109
  * Entry data during route traversal.
@@ -153,10 +120,6 @@ export type {
153
120
  EntryPropSegments,
154
121
  } from "./server/context.js";
155
122
 
156
- // ============================================================================
157
- // Handler Context (Internal)
158
- // ============================================================================
159
-
160
123
  /**
161
124
  * @internal
162
125
  * Internal handler context with additional props for router internals.
@@ -164,30 +127,18 @@ export type {
164
127
  */
165
128
  export type { InternalHandlerContext } from "./types.js";
166
129
 
167
- // ============================================================================
168
- // Rendering (Internal)
169
- // ============================================================================
170
-
171
130
  /**
172
131
  * @internal
173
132
  * Builds React element trees from route segments.
174
133
  */
175
134
  export { renderSegments } from "./segment-system.js";
176
135
 
177
- // ============================================================================
178
- // Error Utilities (Internal)
179
- // ============================================================================
180
-
181
136
  /**
182
137
  * @internal
183
138
  * Error sanitization and network error utilities.
184
139
  */
185
140
  export { sanitizeError, NetworkError, isNetworkError } from "./errors.js";
186
141
 
187
- // ============================================================================
188
- // Type Utilities (Internal)
189
- // ============================================================================
190
-
191
142
  /**
192
143
  * @internal
193
144
  * Scoped view of GeneratedRouteMap for Handler<"localName", ScopedRouteMap<"prefix">>.
@@ -217,10 +168,6 @@ export type {
217
168
  RevalidationDecisionEvent,
218
169
  } from "./router/telemetry.js";
219
170
 
220
- // ============================================================================
221
- // Pre-render / Static Handler Guards (Internal)
222
- // ============================================================================
223
-
224
171
  /**
225
172
  * @internal
226
173
  * Type guard for prerender handler definitions.
@@ -233,20 +180,12 @@ export { isPrerenderHandler, isPassthroughHandler } from "./prerender.js";
233
180
  */
234
181
  export { isStaticHandler } from "./static-handler.js";
235
182
 
236
- // ============================================================================
237
- // URL Pattern Internals
238
- // ============================================================================
239
-
240
183
  /**
241
184
  * @internal
242
185
  * Sentinel used to tag response-type route entries.
243
186
  */
244
187
  export { RESPONSE_TYPE } from "./urls.js";
245
188
 
246
- // ============================================================================
247
- // Route Match Debug (Internal)
248
- // ============================================================================
249
-
250
189
  /**
251
190
  * @internal
252
191
  * Debug utilities for route matching performance analysis.
@@ -256,10 +195,6 @@ export {
256
195
  getMatchDebugStats,
257
196
  } from "./router/pattern-matching.js";
258
197
 
259
- // ============================================================================
260
- // Debug Utilities (Internal)
261
- // ============================================================================
262
-
263
198
  /**
264
199
  * @internal
265
200
  * Debug utilities for manifest inspection and comparison.
@@ -5,7 +5,7 @@ import type { ActionEntry } from "./event-controller.js";
5
5
  * full-update-unsupported cases are handled inline in the bridge before
6
6
  * reconciliation; this only covers successfully-reconciled partial responses.
7
7
  */
8
- export type ActionScenario =
8
+ type ActionScenario =
9
9
  | {
10
10
  type: "navigated-away";
11
11
  historyKeyChanged: boolean;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Action fence: a refcounted flag that is raised while a server action is in
3
+ * flight and lowered when it resolves.
4
+ *
5
+ * It replaces the eager cache clear the action bridge used to do at action
6
+ * start. While the fence is up, the decision of whether the action invalidated
7
+ * anything is deferred to the response:
8
+ *
9
+ * - prefetch consumption is suspended (a queued prefetch result is not served),
10
+ * - a genuine navigation during the flight fetches with `cache: "no-store"` so
11
+ * it cannot be served stale bytes from the browser's Vary-keyed HTTP cache,
12
+ * - popstate reads are treated as stale-while-revalidate.
13
+ *
14
+ * Nothing is wiped, rotated, or broadcast while the fence is up — that is what
15
+ * keeps a sibling tab from seeing a pre-commit signal. Refcounted so concurrent
16
+ * actions compose: each action raises and lowers its own reference, and the
17
+ * fence is down only when the count reaches zero.
18
+ *
19
+ * The refcount is a single module-level counter, not keyed by routerId.
20
+ * Consumers (navigation-client.ts, prefetch/fetch.ts, navigation-bridge.ts)
21
+ * read it unscoped, so an action in one router would suppress another router's
22
+ * navigation/prefetch caches. This is correct only because two routers cannot
23
+ * coexist in one live document: an SPA navigation crossing a host-router
24
+ * boundary forces a full document reload (src/router/request-classification.ts
25
+ * app-switch terminal), so there is always exactly one live router per
26
+ * document. A future multi-router-in-one-document feature must not silently
27
+ * inherit this global, cross-router cache suppression.
28
+ */
29
+
30
+ let fenceCount = 0;
31
+
32
+ export function enterActionFence(): void {
33
+ fenceCount++;
34
+ }
35
+
36
+ export function exitActionFence(): void {
37
+ if (fenceCount > 0) fenceCount--;
38
+ }
39
+
40
+ export function isActionFenceActive(): boolean {
41
+ return fenceCount > 0;
42
+ }
43
+
44
+ /** Test-only: reset the refcount between cases. */
45
+ export function __resetActionFence(): void {
46
+ fenceCount = 0;
47
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Rango state cookie: value codec and attribute serialization.
3
+ *
4
+ * Shared by the client (the document.cookie writer in rango-state.ts) and the
5
+ * server (the Set-Cookie writer in invalidateClientCache). Environment-agnostic:
6
+ * no window/document access and no name composition.
7
+ *
8
+ * Name RESOLUTION deliberately lives elsewhere (router init, see
9
+ * router/state-cookie-name.ts). Keeping composition out of this shared module
10
+ * is what lets the client read the server-resolved name verbatim and compose
11
+ * nothing.
12
+ */
13
+
14
+ /** Default prefix when `stateCookiePrefix` is unset or empty after sanitization. */
15
+ export const DEFAULT_STATE_COOKIE_PREFIX = "rango-state";
16
+
17
+ /** Internal response header carrying the keepClientCache() directive. */
18
+ export const KEEP_CACHE_HEADER = "x-rango-keep-cache";
19
+
20
+ // Per-client signal headers (lower-case) that a SHARED response cache must never
21
+ // store or replay (Finding #3): a `Set-Cookie` (rango state rotation, or any
22
+ // cookie a loader set) and the keepClientCache() directive. Strip-all-Set-Cookie
23
+ // is deliberate — a shared store can't know the resolved cookie name, and any
24
+ // per-client cookie in a cacheable document is the hazard. Single source for the
25
+ // predicate, presence check, and strip below.
26
+ const PER_CLIENT_SIGNAL_HEADERS: readonly string[] = [
27
+ "set-cookie",
28
+ KEEP_CACHE_HEADER,
29
+ ];
30
+
31
+ /** True for a per-client signal header. */
32
+ export function isPerClientSignalHeader(name: string): boolean {
33
+ return PER_CLIENT_SIGNAL_HEADERS.includes(name.toLowerCase());
34
+ }
35
+
36
+ /** True if `headers` carries any per-client signal. */
37
+ export function hasPerClientSignal(headers: Headers): boolean {
38
+ return PER_CLIENT_SIGNAL_HEADERS.some((name) => headers.has(name));
39
+ }
40
+
41
+ /** Remove every per-client signal header from `headers` in place. */
42
+ export function stripPerClientSignals(headers: Headers): void {
43
+ for (const name of PER_CLIENT_SIGNAL_HEADERS) headers.delete(name);
44
+ }
45
+
46
+ /**
47
+ * Read the raw, UNDECODED value of a single cookie from a cookie string (a
48
+ * `document.cookie` jar or a request `Cookie` header). Shared by both seats so
49
+ * the client mirror and the server monotonic-guard read the SAME jar entry —
50
+ * their agreement is the guard's contract. First match wins (the entry the
51
+ * client's mirror holds); exact name boundary via the `=`; an empty value
52
+ * (`name=`) returns null (treated as absent, not a usable prior value).
53
+ */
54
+ export function getRawCookieValue(
55
+ cookieString: string | null,
56
+ name: string,
57
+ ): string | null {
58
+ if (!cookieString) return null;
59
+ const target = name + "=";
60
+ for (const part of cookieString.split(";")) {
61
+ const trimmed = part.trim();
62
+ if (trimmed.startsWith(target)) {
63
+ const value = trimmed.slice(target.length);
64
+ return value === "" ? null : value;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * Encode a state value for the wire: `encodeURIComponent(version):timestamp`.
72
+ * Only the build-derived version is encoded (it is arbitrary); the `:`
73
+ * separator and numeric timestamp stay raw, so the `{version}:{timestamp}`
74
+ * shape survives and `:` is a legal cookie-value octet.
75
+ */
76
+ export function encodeStateValue(version: string, timestamp: number): string {
77
+ return `${encodeURIComponent(version)}:${timestamp}`;
78
+ }
79
+
80
+ /** Parsed state value. `version` is decoded; `timestamp` is the raw integer. */
81
+ export interface StateValue {
82
+ version: string;
83
+ timestamp: number;
84
+ }
85
+
86
+ /**
87
+ * Decode a wire value back into `{version, timestamp}`. Returns null for a
88
+ * malformed value (no `:`, empty version, or non-numeric timestamp) so callers
89
+ * mint fresh instead of trusting garbage.
90
+ */
91
+ export function decodeStateValue(raw: string): StateValue | null {
92
+ const colon = raw.indexOf(":");
93
+ if (colon <= 0) return null;
94
+ const timestamp = Number(raw.slice(colon + 1));
95
+ if (!Number.isFinite(timestamp)) return null;
96
+ let version: string;
97
+ try {
98
+ version = decodeURIComponent(raw.slice(0, colon));
99
+ } catch {
100
+ // A malformed percent-escape (e.g. "%:1") must mint fresh, not throw —
101
+ // a thrown URIError here would 500 the server seat or fail client boot.
102
+ return null;
103
+ }
104
+ return { version, timestamp };
105
+ }
106
+
107
+ /**
108
+ * Mint a fresh state value whose timestamp is strictly greater than the previous
109
+ * one, so a re-mint inside the same millisecond (or a backward clock step) still
110
+ * differs from the current value. `prevRaw` is the current wire value (the
111
+ * client's in-memory mirror, or the server's inbound header/cookie) or null; its
112
+ * timestamp is the floor. Shared by both seats so the monotonic guard lives once.
113
+ */
114
+ export function mintStateValue(
115
+ version: string,
116
+ prevRaw: string | null,
117
+ ): string {
118
+ const prevTs = prevRaw ? (decodeStateValue(prevRaw)?.timestamp ?? 0) : 0;
119
+ const ts = Math.max(Date.now(), prevTs + 1);
120
+ return encodeStateValue(version, ts);
121
+ }
122
+
123
+ /**
124
+ * Attribute string for the state cookie. Session cookie (no Max-Age/Expires),
125
+ * Path=/ (whole app), SameSite=Lax (sent on top-level navigations), and Secure
126
+ * only on https so the document.cookie write does not silently fail on plain
127
+ * http dev. Never HttpOnly (the client reads and writes it).
128
+ */
129
+ export function stateCookieAttributes(secure: boolean): string {
130
+ return `; Path=/; SameSite=Lax${secure ? "; Secure" : ""}`;
131
+ }
132
+
133
+ /** Serialize a full `name=value` cookie string with the state attributes. */
134
+ export function serializeStateCookie(
135
+ name: string,
136
+ value: string,
137
+ secure: boolean,
138
+ ): string {
139
+ return `${name}=${value}${stateCookieAttributes(secure)}`;
140
+ }