@rangojs/router 0.0.0-experimental.d7eeaa75 → 0.0.0-experimental.d98a8e9d

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 (278) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +2154 -861
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +57 -11
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +220 -30
  11. package/skills/caching/SKILL.md +116 -8
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +243 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +57 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +128 -0
  37. package/skills/testing/bindings.md +89 -0
  38. package/skills/testing/cache-prerender.md +98 -0
  39. package/skills/testing/client-components.md +121 -0
  40. package/skills/testing/e2e-parity.md +124 -0
  41. package/skills/testing/flight.md +89 -0
  42. package/skills/testing/handles.md +127 -0
  43. package/skills/testing/loader.md +108 -0
  44. package/skills/testing/middleware.md +97 -0
  45. package/skills/testing/render-handler.md +102 -0
  46. package/skills/testing/response-routes.md +94 -0
  47. package/skills/testing/reverse-and-types.md +83 -0
  48. package/skills/testing/server-actions.md +89 -0
  49. package/skills/testing/server-tree.md +128 -0
  50. package/skills/testing/setup.md +120 -0
  51. package/skills/typesafety/SKILL.md +319 -27
  52. package/skills/use-cache/SKILL.md +34 -5
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/browser/action-coordinator.ts +53 -36
  57. package/src/browser/app-shell.ts +52 -0
  58. package/src/browser/event-controller.ts +86 -70
  59. package/src/browser/history-state.ts +21 -0
  60. package/src/browser/index.ts +3 -3
  61. package/src/browser/navigation-bridge.ts +84 -11
  62. package/src/browser/navigation-client.ts +104 -68
  63. package/src/browser/navigation-store.ts +32 -9
  64. package/src/browser/navigation-transaction.ts +10 -28
  65. package/src/browser/partial-update.ts +64 -26
  66. package/src/browser/prefetch/cache.ts +183 -44
  67. package/src/browser/prefetch/fetch.ts +228 -37
  68. package/src/browser/prefetch/queue.ts +36 -5
  69. package/src/browser/rango-state.ts +53 -13
  70. package/src/browser/react/Link.tsx +30 -2
  71. package/src/browser/react/NavigationProvider.tsx +72 -31
  72. package/src/browser/react/filter-segment-order.ts +51 -7
  73. package/src/browser/react/index.ts +3 -0
  74. package/src/browser/react/location-state-shared.ts +175 -4
  75. package/src/browser/react/location-state.ts +39 -13
  76. package/src/browser/react/use-handle.ts +17 -9
  77. package/src/browser/react/use-navigation.ts +22 -2
  78. package/src/browser/react/use-params.ts +20 -8
  79. package/src/browser/react/use-reverse.ts +106 -0
  80. package/src/browser/react/use-router.ts +22 -2
  81. package/src/browser/react/use-segments.ts +11 -8
  82. package/src/browser/response-adapter.ts +32 -1
  83. package/src/browser/rsc-router.tsx +69 -22
  84. package/src/browser/scroll-restoration.ts +22 -14
  85. package/src/browser/segment-reconciler.ts +36 -14
  86. package/src/browser/segment-structure-assert.ts +2 -2
  87. package/src/browser/server-action-bridge.ts +23 -30
  88. package/src/browser/types.ts +21 -0
  89. package/src/build/collect-fallback-refs.ts +107 -0
  90. package/src/build/generate-manifest.ts +60 -35
  91. package/src/build/generate-route-types.ts +2 -0
  92. package/src/build/index.ts +8 -1
  93. package/src/build/prefix-tree-utils.ts +123 -0
  94. package/src/build/route-trie.ts +95 -25
  95. package/src/build/route-types/codegen.ts +4 -4
  96. package/src/build/route-types/include-resolution.ts +1 -1
  97. package/src/build/route-types/per-module-writer.ts +7 -4
  98. package/src/build/route-types/router-processing.ts +55 -14
  99. package/src/build/route-types/scan-filter.ts +1 -1
  100. package/src/build/route-types/source-scan.ts +118 -0
  101. package/src/build/runtime-discovery.ts +9 -20
  102. package/src/cache/cache-scope.ts +28 -42
  103. package/src/cache/cf/cf-cache-store.ts +54 -13
  104. package/src/client.rsc.tsx +3 -0
  105. package/src/client.tsx +96 -205
  106. package/src/context-var.ts +5 -5
  107. package/src/decode-loader-results.ts +36 -0
  108. package/src/errors.ts +30 -4
  109. package/src/handle.ts +32 -14
  110. package/src/host/index.ts +2 -2
  111. package/src/host/router.ts +129 -57
  112. package/src/host/types.ts +31 -2
  113. package/src/host/utils.ts +1 -1
  114. package/src/href-client.ts +140 -21
  115. package/src/index.rsc.ts +10 -6
  116. package/src/index.ts +54 -17
  117. package/src/loader-store.ts +500 -0
  118. package/src/loader.rsc.ts +25 -7
  119. package/src/loader.ts +16 -9
  120. package/src/missing-id-error.ts +68 -0
  121. package/src/outlet-context.ts +1 -1
  122. package/src/prerender.ts +27 -6
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -36
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +384 -257
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +100 -28
  129. package/src/route-definition/resolve-handler-use.ts +6 -0
  130. package/src/route-definition/use-item-types.ts +32 -0
  131. package/src/route-types.ts +26 -41
  132. package/src/router/basename.ts +14 -0
  133. package/src/router/content-negotiation.ts +15 -2
  134. package/src/router/error-handling.ts +1 -1
  135. package/src/router/find-match.ts +54 -6
  136. package/src/router/handler-context.ts +21 -38
  137. package/src/router/intercept-resolution.ts +4 -18
  138. package/src/router/lazy-includes.ts +41 -22
  139. package/src/router/loader-resolution.ts +82 -36
  140. package/src/router/manifest.ts +41 -19
  141. package/src/router/match-api.ts +4 -3
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/cache-lookup.ts +44 -91
  144. package/src/router/match-middleware/cache-store.ts +3 -2
  145. package/src/router/match-result.ts +53 -32
  146. package/src/router/metrics.ts +1 -1
  147. package/src/router/middleware-types.ts +15 -26
  148. package/src/router/middleware.ts +99 -84
  149. package/src/router/pattern-matching.ts +116 -19
  150. package/src/router/prerender-match.ts +1 -1
  151. package/src/router/preview-match.ts +3 -1
  152. package/src/router/request-classification.ts +4 -28
  153. package/src/router/revalidation.ts +58 -2
  154. package/src/router/router-interfaces.ts +45 -28
  155. package/src/router/router-options.ts +40 -1
  156. package/src/router/router-registry.ts +2 -5
  157. package/src/router/segment-resolution/fresh.ts +27 -6
  158. package/src/router/segment-resolution/revalidation.ts +147 -106
  159. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  160. package/src/router/substitute-pattern-params.ts +56 -0
  161. package/src/router/telemetry.ts +99 -0
  162. package/src/router/trie-matching.ts +40 -16
  163. package/src/router/types.ts +8 -0
  164. package/src/router/url-params.ts +49 -0
  165. package/src/router.ts +52 -30
  166. package/src/rsc/handler-context.ts +2 -2
  167. package/src/rsc/handler.ts +28 -69
  168. package/src/rsc/helpers.ts +91 -43
  169. package/src/rsc/index.ts +1 -1
  170. package/src/rsc/manifest-init.ts +28 -41
  171. package/src/rsc/origin-guard.ts +28 -10
  172. package/src/rsc/progressive-enhancement.ts +4 -0
  173. package/src/rsc/response-error.ts +79 -12
  174. package/src/rsc/response-route-handler.ts +57 -61
  175. package/src/rsc/rsc-rendering.ts +35 -51
  176. package/src/rsc/runtime-warnings.ts +9 -10
  177. package/src/rsc/server-action.ts +17 -37
  178. package/src/rsc/ssr-setup.ts +16 -0
  179. package/src/rsc/types.ts +8 -2
  180. package/src/runtime-env.ts +18 -0
  181. package/src/search-params.ts +4 -4
  182. package/src/segment-content-promise.ts +67 -0
  183. package/src/segment-loader-promise.ts +122 -0
  184. package/src/segment-system.tsx +132 -116
  185. package/src/serialize.ts +243 -0
  186. package/src/server/context.ts +175 -53
  187. package/src/server/cookie-store.ts +28 -4
  188. package/src/server/request-context.ts +67 -51
  189. package/src/ssr/index.tsx +5 -1
  190. package/src/static-handler.ts +25 -3
  191. package/src/testing/cache-status.ts +166 -0
  192. package/src/testing/collect-handle.ts +63 -0
  193. package/src/testing/dispatch.ts +581 -0
  194. package/src/testing/dom.entry.ts +22 -0
  195. package/src/testing/e2e/fixture.ts +188 -0
  196. package/src/testing/e2e/index.ts +149 -0
  197. package/src/testing/e2e/matchers.ts +51 -0
  198. package/src/testing/e2e/page-helpers.ts +272 -0
  199. package/src/testing/e2e/parity.ts +326 -0
  200. package/src/testing/e2e/server.ts +195 -0
  201. package/src/testing/flight-matchers.ts +110 -0
  202. package/src/testing/flight-normalize.ts +38 -0
  203. package/src/testing/flight-runtime.d.ts +57 -0
  204. package/src/testing/flight-tree.ts +682 -0
  205. package/src/testing/flight.entry.ts +51 -0
  206. package/src/testing/flight.ts +234 -0
  207. package/src/testing/generated-routes.ts +223 -0
  208. package/src/testing/index.ts +106 -0
  209. package/src/testing/internal/context.ts +304 -0
  210. package/src/testing/internal/flight-client-globals.ts +30 -0
  211. package/src/testing/internal/seed-vars.ts +42 -0
  212. package/src/testing/render-handler.ts +323 -0
  213. package/src/testing/render-route.tsx +590 -0
  214. package/src/testing/run-loader.ts +363 -0
  215. package/src/testing/run-middleware.ts +205 -0
  216. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  217. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  218. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  219. package/src/testing/vitest-stubs/version.ts +5 -0
  220. package/src/testing/vitest.ts +285 -0
  221. package/src/types/global-namespace.ts +39 -26
  222. package/src/types/handler-context.ts +68 -50
  223. package/src/types/index.ts +1 -0
  224. package/src/types/loader-types.ts +11 -9
  225. package/src/types/request-scope.ts +126 -0
  226. package/src/types/route-entry.ts +11 -0
  227. package/src/types/segments.ts +35 -2
  228. package/src/urls/include-helper.ts +34 -67
  229. package/src/urls/index.ts +1 -5
  230. package/src/urls/path-helper-types.ts +41 -7
  231. package/src/urls/path-helper.ts +17 -52
  232. package/src/urls/pattern-types.ts +36 -19
  233. package/src/urls/response-types.ts +22 -29
  234. package/src/urls/type-extraction.ts +58 -139
  235. package/src/urls/urls-function.ts +1 -5
  236. package/src/use-loader.tsx +413 -42
  237. package/src/vite/debug.ts +185 -0
  238. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  239. package/src/vite/discovery/discover-routers.ts +106 -75
  240. package/src/vite/discovery/discovery-errors.ts +194 -0
  241. package/src/vite/discovery/gate-state.ts +171 -0
  242. package/src/vite/discovery/prerender-collection.ts +67 -26
  243. package/src/vite/discovery/route-types-writer.ts +40 -84
  244. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  245. package/src/vite/discovery/state.ts +33 -0
  246. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  247. package/src/vite/index.ts +2 -0
  248. package/src/vite/plugin-types.ts +67 -0
  249. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  250. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  251. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  252. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  253. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  254. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  255. package/src/vite/plugins/expose-action-id.ts +54 -30
  256. package/src/vite/plugins/expose-id-utils.ts +12 -8
  257. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  258. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  259. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  260. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  261. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  262. package/src/vite/plugins/performance-tracks.ts +29 -25
  263. package/src/vite/plugins/use-cache-transform.ts +65 -50
  264. package/src/vite/plugins/version-injector.ts +39 -23
  265. package/src/vite/plugins/version-plugin.ts +59 -2
  266. package/src/vite/plugins/virtual-entries.ts +2 -2
  267. package/src/vite/rango.ts +116 -29
  268. package/src/vite/router-discovery.ts +750 -100
  269. package/src/vite/utils/ast-handler-extract.ts +15 -15
  270. package/src/vite/utils/banner.ts +1 -1
  271. package/src/vite/utils/bundle-analysis.ts +4 -2
  272. package/src/vite/utils/client-chunks.ts +190 -0
  273. package/src/vite/utils/forward-user-plugins.ts +193 -0
  274. package/src/vite/utils/manifest-utils.ts +8 -59
  275. package/src/vite/utils/package-resolution.ts +41 -1
  276. package/src/vite/utils/prerender-utils.ts +21 -6
  277. package/src/vite/utils/shared-utils.ts +107 -26
  278. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,682 @@
1
+ /// <reference path="./flight-runtime.d.ts" />
2
+ /**
3
+ * renderServerTree — REAL Flight serialize -> deserialize round-trip for unit
4
+ * tests, returning an INSPECTABLE React element tree (not just the wire string).
5
+ *
6
+ * Where it sits:
7
+ * - `renderRoute` (testing/dom): a synthetic CLIENT tree, no Flight at all.
8
+ * - `renderToFlightString` (testing/flight): the real Flight WIRE STRING, for
9
+ * `toMatchFlight` substring/snapshot assertions.
10
+ * - `renderServerTree` (here): serializes the real Flight, then deserializes it
11
+ * back to a React element tree you can traverse — so you can assert TYPED prop
12
+ * fidelity across the server/client boundary (a `Date` comes back a `Date`,
13
+ * not the opaque `$D...` wire encoding) and detect whether a `"use client"`
14
+ * component actually crossed the boundary (an `I` row) or was inlined.
15
+ *
16
+ * Scope (deliberate): serialize + deserialize ONLY. There is NO hydration and
17
+ * NO interaction — the deserialized client boundaries are inert placeholders
18
+ * carrying their props. Real interaction/hydration-mismatch testing stays at the
19
+ * e2e tier; in-process happy-dom hydration re-tests React more than your app and
20
+ * misses the only hydration bug worth a dedicated test (server/client divergence
21
+ * needs a real browser).
22
+ *
23
+ * Runs under the `react-server` export condition, in the SAME worker as the
24
+ * serializer (the client deserializer's react/react-dom imports are inert here
25
+ * because deserialize-only never renders). Use it from the rsc Vitest project
26
+ * (vitest.rsc.config.ts); name files `*.rsc-test.{ts,tsx}`.
27
+ */
28
+
29
+ // MUST be first: defines the webpack-style globals the vendored client reads at
30
+ // module-eval time, before that client module is imported below.
31
+ import "./internal/flight-client-globals.js";
32
+ import { createFromReadableStream } from "@vitejs/plugin-rsc/react/browser";
33
+ import { setRequireModule } from "@vitejs/plugin-rsc/core/browser";
34
+ import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
35
+ import type { ReactNode } from "react";
36
+ import { assertNoLegacyUrlOption, serializeToFlightString } from "./flight.js";
37
+ import type { RenderToFlightStringOptions } from "./flight.js";
38
+
39
+ /** Options for {@link renderServerTree}. */
40
+ export interface RenderServerTreeOptions extends RenderToFlightStringOptions {
41
+ /**
42
+ * The `"use client"` components reachable from the server tree, keyed by the
43
+ * name you want each boundary to have — usually the components you already
44
+ * import to render them: `{ clientComponents: { Counter, PriceTag } }`.
45
+ *
46
+ * The rsc Vitest project does NOT apply the `"use client"` transform, so a
47
+ * plainly-imported island is just a function the serializer would render
48
+ * server-side (and likely throw on a hook). Listing them here registers each as
49
+ * a client reference (in place) so it serializes as a real boundary (`I` row).
50
+ * This depends on NO filename convention — `"use client"` is marked by the
51
+ * directive, not the name, and you already import these to render them.
52
+ *
53
+ * Omit it for pure server-only trees. Components already registered as client
54
+ * references (e.g. by a transform) are left untouched. Registration is in place
55
+ * and per-worker first-wins: a component keeps the first name it is registered
56
+ * under for the rest of the test file.
57
+ */
58
+ clientComponents?: Record<string, unknown>;
59
+ }
60
+
61
+ /** Result of {@link renderServerTree}. */
62
+ export interface RenderServerTreeResult {
63
+ /** The raw Flight wire string (so `toMatchFlight` assertions still apply). */
64
+ flight: string;
65
+ /**
66
+ * The deserialized React element tree. Server elements are plain React
67
+ * elements; each client boundary is an inert placeholder element whose `props`
68
+ * are the real, deserialized JS values that crossed the boundary. Use
69
+ * {@link findClientBoundaries} to locate them.
70
+ */
71
+ tree: unknown;
72
+ }
73
+
74
+ /** A client boundary located in a deserialized tree. */
75
+ export interface ClientBoundary {
76
+ /** The id the boundary was registered under (the `clientComponents` key). */
77
+ id: string;
78
+ /** The boundary name (the `clientComponents` key). */
79
+ name: string;
80
+ /**
81
+ * The props that crossed the boundary, as real deserialized JS values
82
+ * (EXCLUDES `children` — read it off {@link ClientBoundary.children}, mirroring
83
+ * {@link FoundElement}).
84
+ */
85
+ props: Record<string, unknown>;
86
+ /** The boundary's `props.children` (what was nested inside the island). */
87
+ children: unknown;
88
+ /** The raw deserialized element (for advanced assertions). */
89
+ element: unknown;
90
+ }
91
+
92
+ /**
93
+ * A selector for {@link findClientBoundaries}. Pass a string to match by export
94
+ * name (the back-compatible form), or this object to also filter by props /
95
+ * test id / an arbitrary predicate. All provided criteria are AND-ed.
96
+ *
97
+ * Only CLIENT boundaries are matched — a `data-testid` on a `"use client"`
98
+ * island is a prop that crossed the boundary (so `testId` finds it), but a
99
+ * `data-testid` on a plain server host element is NOT a boundary and is not
100
+ * matched here.
101
+ */
102
+ export interface BoundarySelector {
103
+ /** Match the boundary's export name (same as passing a bare string). */
104
+ name?: string;
105
+ /** Match `props["data-testid"]` exactly (sugar over `props: { "data-testid": ... }`). */
106
+ testId?: string;
107
+ /**
108
+ * Subset match: every listed prop must DEEP-EQUAL the boundary's prop of the
109
+ * same key (Date/Map/Set/array/nested-object aware). Props not listed are
110
+ * ignored, so `{ amount: 12.5 }` matches a boundary that also has other props.
111
+ */
112
+ props?: Record<string, unknown>;
113
+ /** Arbitrary predicate, AND-ed with the criteria above. */
114
+ where?: (boundary: ClientBoundary) => boolean;
115
+ }
116
+
117
+ /**
118
+ * Structural equality for boundary-prop matching. Handles the value kinds that
119
+ * survive a Flight round-trip (primitives, Date, Map, Set, Array, plain object);
120
+ * falls back to reference identity for anything exotic.
121
+ *
122
+ * `seen` guards against cyclic input: a deserialized tree can carry a cycle (an
123
+ * element whose props reference an ancestor), and without the guard the
124
+ * recursion blows the stack with a RangeError. When a pair `(a, b)` is already
125
+ * on the active comparison path we treat it as equal — the same
126
+ * reference-identity fallback the docs promise — so a cycle resolves instead of
127
+ * recursing forever.
128
+ */
129
+ function deepEqual(
130
+ a: unknown,
131
+ b: unknown,
132
+ seen: WeakMap<object, WeakSet<object>> = new WeakMap(),
133
+ ): boolean {
134
+ if (Object.is(a, b)) return true;
135
+ if (a instanceof Date && b instanceof Date)
136
+ return a.getTime() === b.getTime();
137
+ // Cycle guard: only objects can recurse, so key on object identity. If we are
138
+ // already comparing this exact pair higher in the stack, stop and treat it as
139
+ // equal (reference-identity fallback).
140
+ if (
141
+ a !== null &&
142
+ b !== null &&
143
+ typeof a === "object" &&
144
+ typeof b === "object"
145
+ ) {
146
+ let partners = seen.get(a as object);
147
+ if (partners?.has(b as object)) return true;
148
+ if (!partners) {
149
+ partners = new WeakSet();
150
+ seen.set(a as object, partners);
151
+ }
152
+ partners.add(b as object);
153
+ }
154
+ if (a instanceof Map && b instanceof Map) {
155
+ if (a.size !== b.size) return false;
156
+ for (const [key, value] of a) {
157
+ if (!b.has(key) || !deepEqual(value, b.get(key), seen)) return false;
158
+ }
159
+ return true;
160
+ }
161
+ if (a instanceof Set && b instanceof Set) {
162
+ if (a.size !== b.size) return false;
163
+ // Deep-equal each member (not shallow has()), so Sets of equal-but-distinct
164
+ // objects/Dates that survived deserialization still match.
165
+ const bValues = [...b];
166
+ for (const value of a) {
167
+ if (!bValues.some((other) => deepEqual(value, other, seen))) return false;
168
+ }
169
+ return true;
170
+ }
171
+ if (Array.isArray(a) && Array.isArray(b)) {
172
+ return (
173
+ a.length === b.length &&
174
+ a.every((value, i) => deepEqual(value, b[i], seen))
175
+ );
176
+ }
177
+ if (
178
+ a !== null &&
179
+ b !== null &&
180
+ typeof a === "object" &&
181
+ typeof b === "object" &&
182
+ !Array.isArray(a) &&
183
+ !Array.isArray(b)
184
+ ) {
185
+ const aKeys = Object.keys(a as object);
186
+ const bKeys = Object.keys(b as object);
187
+ return (
188
+ aKeys.length === bKeys.length &&
189
+ aKeys.every((key) =>
190
+ deepEqual(
191
+ (a as Record<string, unknown>)[key],
192
+ (b as Record<string, unknown>)[key],
193
+ seen,
194
+ ),
195
+ )
196
+ );
197
+ }
198
+ return false;
199
+ }
200
+
201
+ /** Does a boundary satisfy every criterion of an object selector? */
202
+ function matchesSelector(
203
+ boundary: ClientBoundary,
204
+ selector: BoundarySelector,
205
+ ): boolean {
206
+ if (selector.name !== undefined && boundary.name !== selector.name) {
207
+ return false;
208
+ }
209
+ if (
210
+ selector.testId !== undefined &&
211
+ boundary.props["data-testid"] !== selector.testId
212
+ ) {
213
+ return false;
214
+ }
215
+ if (selector.props !== undefined) {
216
+ for (const [key, value] of Object.entries(selector.props)) {
217
+ if (!deepEqual(boundary.props[key], value)) return false;
218
+ }
219
+ }
220
+ if (selector.where !== undefined && !selector.where(boundary)) {
221
+ return false;
222
+ }
223
+ return true;
224
+ }
225
+
226
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
227
+
228
+ /**
229
+ * Tag a value as a client reference in place, unless it already is one. Accepts
230
+ * both functions and component OBJECTS — `memo(...)` / `forwardRef(...)` exports
231
+ * are objects at runtime, so a function-only check would skip them and the
232
+ * serializer would inline them server-side instead of emitting an `I` row. ESM
233
+ * live-binding identity means the server tree's own import of the same value then
234
+ * sees the reference, so it serializes as a boundary.
235
+ */
236
+ function registerOne(value: unknown, id: string, exportName: string): void {
237
+ if (value === null) return;
238
+ const kind = typeof value;
239
+ if (kind !== "function" && kind !== "object") return;
240
+ const ref = value as { $$typeof?: symbol; $$id?: string };
241
+ if (ref.$$typeof === CLIENT_REFERENCE || ref.$$id) return;
242
+ RSDServer.registerClientReference(value, id, exportName);
243
+ }
244
+
245
+ /** Register `{ name: Component }` entries, keyed by name (id === name). */
246
+ export function registerClientComponents(
247
+ components: Record<string, unknown>,
248
+ ): void {
249
+ for (const [name, value] of Object.entries(components)) {
250
+ registerOne(value, name, name);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * A client manifest that resolves ANY registered client reference without
256
+ * needing to enumerate them. `$$id` is `${id}#${exportName}`; the serializer
257
+ * looks it up here. We omit `async`, so each `I` row is a 3-element row and the
258
+ * deserialized boundary's payload is a clean `resolved_module` whose value is
259
+ * `[id, [], name]` — synchronously readable by {@link findClientBoundaries}.
260
+ */
261
+ export function makeClientManifest(): unknown {
262
+ return new Proxy(
263
+ {},
264
+ {
265
+ get(_target, key) {
266
+ if (typeof key !== "string") return undefined;
267
+ const hash = key.lastIndexOf("#");
268
+ const id = hash >= 0 ? key.slice(0, hash) : key;
269
+ const name = hash >= 0 ? key.slice(hash + 1) : "default";
270
+ return { id, chunks: [], name };
271
+ },
272
+ },
273
+ );
274
+ }
275
+
276
+ // Module-level "init once" flag. Safe ONLY because the Flight test project runs
277
+ // under `pool: "forks"` (see the shipped vitest.rsc.config.ts template): each
278
+ // test FILE gets its own process, so the flag is fresh per file. Under
279
+ // `pool: "threads"` (or any shared-worker pool) the loader would be installed
280
+ // once and silently reused across files — keep the Flight project on `forks`.
281
+ let loadInstalled = false;
282
+ /**
283
+ * Install the deserialize-side module loader. For the non-async manifest above,
284
+ * the deserializer never calls it (boundaries stay placeholders), so it throws a
285
+ * clear error if a code path ever does try to execute a client reference.
286
+ */
287
+ function installDeserializeLoad(): void {
288
+ if (loadInstalled) return;
289
+ loadInstalled = true;
290
+ setRequireModule({
291
+ load: (id: string) => {
292
+ throw new Error(
293
+ `renderServerTree does not execute client references (deserialize-only). ` +
294
+ `A client reference "${id}" was resolved — render/interaction is the e2e tier.`,
295
+ );
296
+ },
297
+ });
298
+ }
299
+
300
+ function stringToStream(text: string): ReadableStream<Uint8Array> {
301
+ // TextEncoder replaces invalid UTF-16 (unmatched surrogates) with U+FFFD. Flight
302
+ // strings come from the serializer, so this is not a concern in practice; a
303
+ // mangled input would surface downstream as a deserialize error, not here.
304
+ const bytes = new TextEncoder().encode(text);
305
+ return new ReadableStream({
306
+ start(controller) {
307
+ controller.enqueue(bytes);
308
+ controller.close();
309
+ },
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Serialize a server component to real Flight, then deserialize it back to an
315
+ * inspectable React element tree. See the module header for scope.
316
+ *
317
+ * Must run under the `react-server` export condition.
318
+ */
319
+ export async function renderServerTree(
320
+ element: ReactNode,
321
+ opts: RenderServerTreeOptions = {},
322
+ ): Promise<RenderServerTreeResult> {
323
+ assertNoLegacyUrlOption(opts, "renderServerTree");
324
+ if (opts.clientComponents) registerClientComponents(opts.clientComponents);
325
+ const flight = await serializeToFlightString(
326
+ element,
327
+ opts,
328
+ makeClientManifest(),
329
+ );
330
+ return { flight, tree: await deserializeFlight(flight) };
331
+ }
332
+
333
+ /**
334
+ * Deserialize a Flight wire string back to an inspectable React element tree:
335
+ * `createFromReadableStream` (vendored client), then unwrap Rango's payload
336
+ * wrapper and resolve the top server chunk so the consumer gets their element,
337
+ * not a lazy. Reused by renderServerTree AND renderHandler. Client references
338
+ * stay as inert boundary markers ({@link findClientBoundaries} reads them).
339
+ */
340
+ export async function deserializeFlight(flight: string): Promise<unknown> {
341
+ installDeserializeLoad();
342
+ const payload = await createFromReadableStream(stringToStream(flight));
343
+ return resolveServerLazy(unwrapPayload(payload));
344
+ }
345
+
346
+ /**
347
+ * The serializer wraps the element in Rango's payload shape
348
+ * (`{ metadata: { segments: [{ component }] } }`) to mirror the real wire
349
+ * format. Return the consumer's own element tree, not that wrapper.
350
+ */
351
+ function unwrapPayload(payload: unknown): unknown {
352
+ const segment = (
353
+ payload as { metadata?: { segments?: Array<{ component?: unknown }> } }
354
+ )?.metadata?.segments?.[0];
355
+ return segment && "component" in segment ? segment.component : payload;
356
+ }
357
+
358
+ interface FlightLazy {
359
+ _payload: { status: string; value: unknown };
360
+ _init: (payload: unknown) => unknown;
361
+ }
362
+
363
+ function asFlightLazy(node: unknown): FlightLazy | undefined {
364
+ const candidate = node as Partial<FlightLazy> | null;
365
+ if (
366
+ candidate &&
367
+ typeof candidate === "object" &&
368
+ typeof candidate._init === "function" &&
369
+ candidate._payload &&
370
+ typeof (candidate._payload as { status?: unknown }).status === "string"
371
+ ) {
372
+ return candidate as FlightLazy;
373
+ }
374
+ return undefined;
375
+ }
376
+
377
+ /**
378
+ * An async server component serializes as a deferred chunk that deserializes to
379
+ * a lazy (`status: "resolved_model"`). Initialize it to the materialized element
380
+ * (synchronous for a fully-drained stream; never calls a client `load`). Client
381
+ * references (`status: "resolved_module"`) are left untouched — they are the
382
+ * boundary markers {@link findClientBoundaries} reads.
383
+ *
384
+ * Why `"fulfilled"` is also accepted: the vendored client's readChunk (which is
385
+ * the lazy's `_init`) is destructive — the FIRST read transitions the shared
386
+ * chunk `resolved_model` -> `fulfilled` and stores the materialized value on
387
+ * `_payload.value` (see initializeModelChunk in the vendored client). A second
388
+ * encounter of the same lazy (textContent called twice, or a parent host
389
+ * element materializing the chunk before a child reads it within one walk) then
390
+ * sees `fulfilled`. Pre-fix this guard bailed on `fulfilled` and returned the
391
+ * lazy wrapper, so the subtree was silently skipped (textContent went ""; a
392
+ * findElements text selector missed). Initializing a `fulfilled` chunk is a
393
+ * no-op that returns `_payload.value`, so accepting it makes resolution
394
+ * idempotent.
395
+ */
396
+ function resolveServerLazy(node: unknown): unknown {
397
+ let current = node;
398
+ for (let guard = 0; guard < 1000; guard++) {
399
+ const lazy = asFlightLazy(current);
400
+ const status = lazy?._payload.status;
401
+ if (!lazy || (status !== "resolved_model" && status !== "fulfilled")) {
402
+ return current;
403
+ }
404
+ try {
405
+ current = lazy._init(lazy._payload);
406
+ } catch {
407
+ return current;
408
+ }
409
+ }
410
+ return current;
411
+ }
412
+
413
+ interface ClientBoundaryElement {
414
+ type: { _payload: { status: string; value: unknown[] } };
415
+ props?: Record<string, unknown>;
416
+ }
417
+
418
+ function isClientBoundaryElement(node: unknown): node is ClientBoundaryElement {
419
+ const type = (node as { type?: unknown })?.type as
420
+ | { _payload?: { status?: string; value?: unknown } }
421
+ | undefined;
422
+ const payload = type?._payload;
423
+ return (
424
+ !!payload &&
425
+ payload.status === "resolved_module" &&
426
+ Array.isArray(payload.value)
427
+ );
428
+ }
429
+
430
+ /**
431
+ * Walk a deserialized tree and return every client boundary, in document order,
432
+ * each with its id, export name, and typed props. The optional second arg
433
+ * filters the result:
434
+ * - a STRING matches by export name (`findClientBoundaries(tree, "PriceTag")`);
435
+ * - a {@link BoundarySelector} object filters by `name` / `testId` / `props`
436
+ * (subset deep-equal) / `where` predicate, AND-ed
437
+ * (`findClientBoundaries(tree, { testId: "price-tag" })`).
438
+ *
439
+ * Always returns an array (no throw on zero/many). For a single expected
440
+ * boundary, destructure the first: `const [tag] = findClientBoundaries(tree,
441
+ * "PriceTag")` — and assert on `.length` when the count matters (no match
442
+ * yields `[]`, so `tag` would be `undefined`).
443
+ */
444
+ /**
445
+ * Walk a deserialized tree, calling `visit` on every materialized object node in
446
+ * document order (parent before children). Async-server-component chunks are
447
+ * materialized via resolveServerLazy so the walk descends into them; arrays are
448
+ * traversed but not themselves visited. Shared by findClientBoundaries and
449
+ * findElements.
450
+ */
451
+ function walkNodes(tree: unknown, visit: (node: object) => void): void {
452
+ const seen = new Set<unknown>();
453
+ const recur = (raw: unknown): void => {
454
+ const node = resolveServerLazy(raw);
455
+ if (node == null || typeof node !== "object") return;
456
+ if (seen.has(node)) return;
457
+ seen.add(node);
458
+ if (Array.isArray(node)) {
459
+ for (const child of node) recur(child);
460
+ return;
461
+ }
462
+ visit(node);
463
+ // Recurse all own enumerable values: the tree is a deserialized payload
464
+ // (metadata -> segments -> component -> children/props), not just nested
465
+ // React props.
466
+ for (const value of Object.values(node as Record<string, unknown>)) {
467
+ recur(value);
468
+ }
469
+ };
470
+ recur(tree);
471
+ }
472
+
473
+ export function findClientBoundaries(
474
+ tree: unknown,
475
+ selector?: string | BoundarySelector,
476
+ ): ClientBoundary[] {
477
+ const out: ClientBoundary[] = [];
478
+ walkNodes(tree, (node) => {
479
+ if (isClientBoundaryElement(node)) {
480
+ const value = node.type._payload.value;
481
+ const { children, ...rest } = (node.props ?? {}) as Record<
482
+ string,
483
+ unknown
484
+ >;
485
+ out.push({
486
+ id: String(value[0]),
487
+ name: String(value[2]),
488
+ props: rest,
489
+ children,
490
+ element: node,
491
+ });
492
+ }
493
+ });
494
+ if (selector === undefined) return out;
495
+ if (typeof selector === "string") {
496
+ return out.filter((boundary) => boundary.name === selector);
497
+ }
498
+ return out.filter((boundary) => matchesSelector(boundary, selector));
499
+ }
500
+
501
+ /** A server/host element located in a deserialized tree by {@link findElements}. */
502
+ export interface FoundElement {
503
+ /** The host tag name (`"article"`, `"h2"`). Always a host element. */
504
+ tag: string;
505
+ /** The element's props, as real deserialized JS values (excludes `children`). */
506
+ props: Record<string, unknown>;
507
+ /** The element's `props.children` (the rendered child tree), for convenience. */
508
+ children: unknown;
509
+ /** Concatenated text content of this element's subtree. */
510
+ text: string;
511
+ /** The raw deserialized element (for advanced assertions). */
512
+ element: unknown;
513
+ }
514
+
515
+ /**
516
+ * A selector for {@link findElements}. Pass a string to match a host tag name
517
+ * (`"h2"`), or this object for finer matches. All provided criteria are AND-ed.
518
+ *
519
+ * Mirrors {@link BoundarySelector} but keys on `tag` (the host tag) rather than
520
+ * `name` (a client component's export identity) — by design, since a host element
521
+ * has no component name. It also adds `text`, which a boundary selector lacks: a
522
+ * host element has rendered text, whereas a client boundary is an inert
523
+ * placeholder with no rendered children to match against.
524
+ */
525
+ export interface ElementSelector {
526
+ /** Match the host tag name (`"article"`, `"h2"`). */
527
+ tag?: string;
528
+ /** Match `props["data-testid"]` exactly. */
529
+ testId?: string;
530
+ /** Subset deep-equal match on props (Date/Map/Set/array/nested aware). */
531
+ props?: Record<string, unknown>;
532
+ /** Match the element's text content (substring for a string, `.test()` for a RegExp). */
533
+ text?: string | RegExp;
534
+ /** Arbitrary predicate, AND-ed with the criteria above. */
535
+ where?: (element: FoundElement) => boolean;
536
+ }
537
+
538
+ // React 19 stamps elements with `react.transitional.element`; `react.element` is
539
+ // the React 18 symbol. Accept both so the check is robust across React majors.
540
+ // This `$$typeof` test is load-bearing: it distinguishes a real element from a
541
+ // plain payload object that merely has a string `type` field (e.g. an input's
542
+ // `props` object `{ type: "text" }`), which would otherwise look like a host element.
543
+ const REACT_ELEMENT = Symbol.for("react.element");
544
+ const REACT_TRANSITIONAL_ELEMENT = Symbol.for("react.transitional.element");
545
+
546
+ /** Is a node a React element (host or component), as opposed to a plain object? */
547
+ function isReactElement(
548
+ node: object,
549
+ ): node is { type: unknown; props?: Record<string, unknown> } {
550
+ const tag = (node as { $$typeof?: symbol }).$$typeof;
551
+ return (
552
+ (tag === REACT_ELEMENT || tag === REACT_TRANSITIONAL_ELEMENT) &&
553
+ "type" in node
554
+ );
555
+ }
556
+
557
+ /**
558
+ * Concatenate the text content of a deserialized node's subtree — every string
559
+ * and number leaf, in document order, space-free (`<h2>Wine {2}</h2>` ->
560
+ * `"Wine 2"` only if the source had the space). Use it to assert rendered text
561
+ * without reaching for `JSON.stringify(tree).toContain(...)`.
562
+ */
563
+ export function textContent(node: unknown): string {
564
+ let out = "";
565
+ const recur = (raw: unknown): void => {
566
+ const value = resolveServerLazy(raw);
567
+ if (value == null || typeof value === "boolean") return;
568
+ if (typeof value === "string") {
569
+ out += value;
570
+ return;
571
+ }
572
+ if (typeof value === "number" || typeof value === "bigint") {
573
+ // React renders both numbers and bigints as text; mirror that here so a
574
+ // {2n} leaf is counted like a {2} leaf.
575
+ out += String(value);
576
+ return;
577
+ }
578
+ if (Array.isArray(value)) {
579
+ for (const child of value) recur(child);
580
+ return;
581
+ }
582
+ if (typeof value === "object") {
583
+ // A React element: descend into its children only (not props/type), so a
584
+ // string-valued prop like className is not counted as text.
585
+ if (isReactElement(value)) {
586
+ recur((value.props as { children?: unknown })?.children);
587
+ }
588
+ }
589
+ };
590
+ recur(node);
591
+ return out;
592
+ }
593
+
594
+ /**
595
+ * Walk a deserialized tree and return every SERVER/HOST element (the output a
596
+ * server component rendered), in document order. The optional second arg filters:
597
+ * - a STRING matches a host tag name (`findElements(tree, "h2")`);
598
+ * - an {@link ElementSelector} filters by `tag` / `testId` / `props` (subset
599
+ * deep-equal) / `text` (substring or RegExp) / `where`, AND-ed.
600
+ *
601
+ * Caveat: server COMPONENTS do not survive Flight as identities — they are
602
+ * executed during serialization, so only the host elements they produced remain.
603
+ * Match those host elements (by tag/props/text), not the component function. For
604
+ * CLIENT islands (which DO keep identity) use {@link findClientBoundaries}.
605
+ *
606
+ * Always returns an array (destructure the first for a single expected match).
607
+ */
608
+ export function findElements(
609
+ tree: unknown,
610
+ selector?: string | ElementSelector,
611
+ ): FoundElement[] {
612
+ const out: FoundElement[] = [];
613
+ walkNodes(tree, (node) => {
614
+ // Host elements only (typeof type === "string"). Excludes: client boundaries
615
+ // (type is a lazy module placeholder -> findClientBoundaries), fragments and
616
+ // other component elements (type is a Symbol/function), and plain payload
617
+ // objects (isReactElement guards against an object whose `type` is a string
618
+ // prop, like an input's `{ type: "text" }`).
619
+ if (!isReactElement(node) || typeof node.type !== "string") return;
620
+ const props = (node.props ?? {}) as Record<string, unknown>;
621
+ const { children, ...rest } = props;
622
+ out.push({
623
+ tag: node.type,
624
+ props: rest,
625
+ children,
626
+ text: textContent(node),
627
+ element: node,
628
+ });
629
+ });
630
+ if (selector === undefined) return out;
631
+ if (typeof selector === "string") {
632
+ return out.filter((element) => element.tag === selector);
633
+ }
634
+ return out.filter((element) => matchesElementSelector(element, selector));
635
+ }
636
+
637
+ /** Does a found element satisfy every criterion of an object selector? */
638
+ function matchesElementSelector(
639
+ element: FoundElement,
640
+ selector: ElementSelector,
641
+ ): boolean {
642
+ if (selector.tag !== undefined && element.tag !== selector.tag) return false;
643
+ if (
644
+ selector.testId !== undefined &&
645
+ element.props["data-testid"] !== selector.testId
646
+ ) {
647
+ return false;
648
+ }
649
+ if (selector.props !== undefined) {
650
+ for (const [key, value] of Object.entries(selector.props)) {
651
+ if (!deepEqual(element.props[key], value)) return false;
652
+ }
653
+ }
654
+ if (selector.text !== undefined) {
655
+ const matched =
656
+ typeof selector.text === "string"
657
+ ? element.text.includes(selector.text)
658
+ : selector.text.test(element.text);
659
+ if (!matched) return false;
660
+ }
661
+ if (selector.where !== undefined && !selector.where(element)) return false;
662
+ return true;
663
+ }
664
+
665
+ /**
666
+ * Smoke check that the vendored client deserializer subpaths still resolve. The
667
+ * paths are private to plugin-rsc; a minor bump could relocate them. Call this in
668
+ * a test to fail loudly with a clear message instead of an opaque import error.
669
+ */
670
+ export function assertFlightTreeRuntimeAvailable(): void {
671
+ if (
672
+ typeof createFromReadableStream !== "function" ||
673
+ typeof setRequireModule !== "function" ||
674
+ typeof RSDServer.registerClientReference !== "function"
675
+ ) {
676
+ throw new Error(
677
+ "renderServerTree runtime not available: a @vitejs/plugin-rsc client/server " +
678
+ "subpath did not export the expected function. A plugin-rsc upgrade may have " +
679
+ "moved react/browser, core/browser, or vendor/react-server-dom/server.edge.",
680
+ );
681
+ }
682
+ }