@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7d061845

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 (252) 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 +2138 -841
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +68 -21
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/document-cache/SKILL.md +78 -55
  13. package/skills/handler-use/SKILL.md +3 -1
  14. package/skills/hooks/SKILL.md +229 -20
  15. package/skills/host-router/SKILL.md +45 -20
  16. package/skills/i18n/SKILL.md +276 -0
  17. package/skills/intercept/SKILL.md +26 -4
  18. package/skills/layout/SKILL.md +6 -7
  19. package/skills/links/SKILL.md +247 -17
  20. package/skills/loader/SKILL.md +219 -9
  21. package/skills/middleware/SKILL.md +15 -9
  22. package/skills/migrate-nextjs/SKILL.md +4 -2
  23. package/skills/migrate-react-router/SKILL.md +5 -0
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +12 -6
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +242 -24
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +66 -9
  31. package/skills/route/SKILL.md +33 -4
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +751 -0
  34. package/skills/streams-and-websockets/SKILL.md +283 -0
  35. package/skills/testing/SKILL.md +816 -0
  36. package/skills/typesafety/SKILL.md +319 -27
  37. package/skills/use-cache/SKILL.md +34 -5
  38. package/skills/view-transitions/SKILL.md +294 -0
  39. package/src/__augment-tests__/augment.ts +81 -0
  40. package/src/__augment-tests__/augmented.check.ts +117 -0
  41. package/src/browser/action-coordinator.ts +53 -36
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/event-controller.ts +86 -70
  44. package/src/browser/history-state.ts +21 -0
  45. package/src/browser/index.ts +3 -3
  46. package/src/browser/navigation-bridge.ts +65 -9
  47. package/src/browser/navigation-client.ts +45 -25
  48. package/src/browser/navigation-store.ts +32 -9
  49. package/src/browser/navigation-transaction.ts +10 -28
  50. package/src/browser/partial-update.ts +52 -26
  51. package/src/browser/prefetch/cache.ts +124 -26
  52. package/src/browser/prefetch/fetch.ts +114 -38
  53. package/src/browser/prefetch/queue.ts +36 -5
  54. package/src/browser/rango-state.ts +53 -13
  55. package/src/browser/react/Link.tsx +18 -13
  56. package/src/browser/react/NavigationProvider.tsx +72 -31
  57. package/src/browser/react/filter-segment-order.ts +51 -7
  58. package/src/browser/react/index.ts +3 -0
  59. package/src/browser/react/location-state-shared.ts +175 -4
  60. package/src/browser/react/location-state.ts +39 -13
  61. package/src/browser/react/use-handle.ts +17 -9
  62. package/src/browser/react/use-navigation.ts +22 -2
  63. package/src/browser/react/use-params.ts +20 -8
  64. package/src/browser/react/use-reverse.ts +106 -0
  65. package/src/browser/react/use-router.ts +22 -2
  66. package/src/browser/react/use-segments.ts +11 -8
  67. package/src/browser/response-adapter.ts +25 -0
  68. package/src/browser/rsc-router.tsx +64 -22
  69. package/src/browser/scroll-restoration.ts +22 -14
  70. package/src/browser/segment-structure-assert.ts +2 -2
  71. package/src/browser/server-action-bridge.ts +23 -30
  72. package/src/browser/types.ts +21 -0
  73. package/src/build/collect-fallback-refs.ts +107 -0
  74. package/src/build/generate-manifest.ts +60 -35
  75. package/src/build/generate-route-types.ts +2 -0
  76. package/src/build/index.ts +2 -0
  77. package/src/build/route-trie.ts +2 -1
  78. package/src/build/route-types/codegen.ts +4 -4
  79. package/src/build/route-types/include-resolution.ts +1 -1
  80. package/src/build/route-types/per-module-writer.ts +7 -4
  81. package/src/build/route-types/router-processing.ts +55 -14
  82. package/src/build/route-types/scan-filter.ts +1 -1
  83. package/src/build/route-types/source-scan.ts +118 -0
  84. package/src/build/runtime-discovery.ts +9 -20
  85. package/src/cache/cache-scope.ts +28 -42
  86. package/src/cache/cf/cf-cache-store.ts +54 -13
  87. package/src/client.rsc.tsx +3 -0
  88. package/src/client.tsx +10 -8
  89. package/src/context-var.ts +5 -5
  90. package/src/decode-loader-results.ts +36 -0
  91. package/src/errors.ts +30 -1
  92. package/src/handle.ts +26 -13
  93. package/src/host/index.ts +2 -2
  94. package/src/host/router.ts +129 -57
  95. package/src/host/types.ts +31 -2
  96. package/src/host/utils.ts +1 -1
  97. package/src/href-client.ts +140 -20
  98. package/src/index.rsc.ts +9 -4
  99. package/src/index.ts +16 -6
  100. package/src/loader-store.ts +500 -0
  101. package/src/loader.rsc.ts +21 -6
  102. package/src/loader.ts +3 -10
  103. package/src/missing-id-error.ts +68 -0
  104. package/src/outlet-context.ts +1 -1
  105. package/src/prerender.ts +4 -4
  106. package/src/response-utils.ts +37 -0
  107. package/src/reverse.ts +65 -39
  108. package/src/route-content-wrapper.tsx +6 -28
  109. package/src/route-definition/dsl-helpers.ts +253 -265
  110. package/src/route-definition/helper-factories.ts +29 -139
  111. package/src/route-definition/helpers-types.ts +43 -15
  112. package/src/route-definition/resolve-handler-use.ts +6 -0
  113. package/src/route-definition/use-item-types.ts +32 -0
  114. package/src/route-types.ts +19 -41
  115. package/src/router/basename.ts +14 -0
  116. package/src/router/content-negotiation.ts +15 -2
  117. package/src/router/error-handling.ts +1 -1
  118. package/src/router/handler-context.ts +21 -41
  119. package/src/router/intercept-resolution.ts +4 -18
  120. package/src/router/lazy-includes.ts +3 -3
  121. package/src/router/loader-resolution.ts +19 -2
  122. package/src/router/match-api.ts +4 -3
  123. package/src/router/match-handlers.ts +63 -20
  124. package/src/router/match-middleware/cache-lookup.ts +44 -91
  125. package/src/router/match-middleware/cache-store.ts +3 -2
  126. package/src/router/match-result.ts +53 -32
  127. package/src/router/metrics.ts +1 -1
  128. package/src/router/middleware-types.ts +15 -26
  129. package/src/router/middleware.ts +99 -84
  130. package/src/router/pattern-matching.ts +101 -17
  131. package/src/router/prerender-match.ts +1 -1
  132. package/src/router/preview-match.ts +3 -1
  133. package/src/router/request-classification.ts +4 -28
  134. package/src/router/revalidation.ts +58 -2
  135. package/src/router/router-interfaces.ts +45 -28
  136. package/src/router/router-options.ts +40 -1
  137. package/src/router/router-registry.ts +2 -5
  138. package/src/router/segment-resolution/fresh.ts +27 -6
  139. package/src/router/segment-resolution/revalidation.ts +147 -106
  140. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  141. package/src/router/substitute-pattern-params.ts +56 -0
  142. package/src/router/telemetry.ts +99 -0
  143. package/src/router/trie-matching.ts +18 -13
  144. package/src/router/types.ts +8 -0
  145. package/src/router/url-params.ts +49 -0
  146. package/src/router.ts +38 -23
  147. package/src/rsc/handler-context.ts +2 -2
  148. package/src/rsc/handler.ts +28 -69
  149. package/src/rsc/helpers.ts +91 -43
  150. package/src/rsc/index.ts +1 -1
  151. package/src/rsc/origin-guard.ts +28 -10
  152. package/src/rsc/progressive-enhancement.ts +4 -0
  153. package/src/rsc/response-route-handler.ts +46 -53
  154. package/src/rsc/rsc-rendering.ts +35 -51
  155. package/src/rsc/runtime-warnings.ts +9 -10
  156. package/src/rsc/server-action.ts +17 -37
  157. package/src/rsc/ssr-setup.ts +16 -0
  158. package/src/rsc/types.ts +8 -2
  159. package/src/search-params.ts +4 -4
  160. package/src/segment-system.tsx +122 -56
  161. package/src/serialize.ts +243 -0
  162. package/src/server/context.ts +118 -51
  163. package/src/server/cookie-store.ts +28 -4
  164. package/src/server/request-context.ts +20 -42
  165. package/src/ssr/index.tsx +5 -1
  166. package/src/static-handler.ts +1 -1
  167. package/src/testing/cache-status.ts +166 -0
  168. package/src/testing/collect-handle.ts +63 -0
  169. package/src/testing/dispatch.ts +440 -0
  170. package/src/testing/dom.entry.ts +22 -0
  171. package/src/testing/e2e/fixture.ts +154 -0
  172. package/src/testing/e2e/index.ts +149 -0
  173. package/src/testing/e2e/matchers.ts +51 -0
  174. package/src/testing/e2e/page-helpers.ts +272 -0
  175. package/src/testing/e2e/parity.ts +306 -0
  176. package/src/testing/e2e/server.ts +183 -0
  177. package/src/testing/flight-matchers.ts +104 -0
  178. package/src/testing/flight-runtime.d.ts +57 -0
  179. package/src/testing/flight-tree.ts +332 -0
  180. package/src/testing/flight.entry.ts +46 -0
  181. package/src/testing/flight.ts +224 -0
  182. package/src/testing/generated-routes.ts +223 -0
  183. package/src/testing/index.ts +106 -0
  184. package/src/testing/internal/context.ts +304 -0
  185. package/src/testing/internal/flight-client-globals.ts +30 -0
  186. package/src/testing/internal/seed-vars.ts +42 -0
  187. package/src/testing/render-handler.ts +267 -0
  188. package/src/testing/render-route.tsx +565 -0
  189. package/src/testing/run-loader.ts +341 -0
  190. package/src/testing/run-middleware.ts +188 -0
  191. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  192. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  193. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  194. package/src/testing/vitest-stubs/version.ts +5 -0
  195. package/src/testing/vitest.ts +270 -0
  196. package/src/types/global-namespace.ts +39 -26
  197. package/src/types/handler-context.ts +68 -50
  198. package/src/types/index.ts +1 -0
  199. package/src/types/loader-types.ts +5 -6
  200. package/src/types/request-scope.ts +126 -0
  201. package/src/types/segments.ts +35 -1
  202. package/src/urls/include-helper.ts +10 -53
  203. package/src/urls/index.ts +0 -3
  204. package/src/urls/path-helper-types.ts +11 -3
  205. package/src/urls/path-helper.ts +17 -52
  206. package/src/urls/pattern-types.ts +36 -19
  207. package/src/urls/response-types.ts +22 -29
  208. package/src/urls/type-extraction.ts +26 -116
  209. package/src/urls/urls-function.ts +1 -5
  210. package/src/use-loader.tsx +413 -42
  211. package/src/vite/debug.ts +185 -0
  212. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  213. package/src/vite/discovery/discover-routers.ts +101 -51
  214. package/src/vite/discovery/discovery-errors.ts +194 -0
  215. package/src/vite/discovery/gate-state.ts +171 -0
  216. package/src/vite/discovery/prerender-collection.ts +67 -26
  217. package/src/vite/discovery/route-types-writer.ts +40 -84
  218. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  219. package/src/vite/discovery/state.ts +33 -0
  220. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  221. package/src/vite/index.ts +2 -0
  222. package/src/vite/plugin-types.ts +67 -0
  223. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  224. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  225. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  226. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  227. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  228. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  229. package/src/vite/plugins/expose-action-id.ts +54 -30
  230. package/src/vite/plugins/expose-id-utils.ts +12 -8
  231. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  232. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  233. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  234. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  235. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  236. package/src/vite/plugins/performance-tracks.ts +29 -25
  237. package/src/vite/plugins/use-cache-transform.ts +65 -50
  238. package/src/vite/plugins/version-injector.ts +39 -23
  239. package/src/vite/plugins/version-plugin.ts +59 -2
  240. package/src/vite/plugins/virtual-entries.ts +2 -2
  241. package/src/vite/rango.ts +116 -29
  242. package/src/vite/router-discovery.ts +750 -100
  243. package/src/vite/utils/ast-handler-extract.ts +15 -15
  244. package/src/vite/utils/banner.ts +1 -1
  245. package/src/vite/utils/bundle-analysis.ts +4 -2
  246. package/src/vite/utils/client-chunks.ts +190 -0
  247. package/src/vite/utils/forward-user-plugins.ts +193 -0
  248. package/src/vite/utils/manifest-utils.ts +21 -5
  249. package/src/vite/utils/package-resolution.ts +41 -1
  250. package/src/vite/utils/prerender-utils.ts +5 -4
  251. package/src/vite/utils/shared-utils.ts +107 -26
  252. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,332 @@
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 { 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
+ /** The props that crossed the boundary, as real deserialized JS values. */
81
+ props: Record<string, unknown>;
82
+ /** The raw deserialized element (for advanced assertions). */
83
+ element: unknown;
84
+ }
85
+
86
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
87
+
88
+ /**
89
+ * Tag a value as a client reference in place, unless it already is one. Accepts
90
+ * both functions and component OBJECTS — `memo(...)` / `forwardRef(...)` exports
91
+ * are objects at runtime, so a function-only check would skip them and the
92
+ * serializer would inline them server-side instead of emitting an `I` row. ESM
93
+ * live-binding identity means the server tree's own import of the same value then
94
+ * sees the reference, so it serializes as a boundary.
95
+ */
96
+ function registerOne(value: unknown, id: string, exportName: string): void {
97
+ if (value === null) return;
98
+ const kind = typeof value;
99
+ if (kind !== "function" && kind !== "object") return;
100
+ const ref = value as { $$typeof?: symbol; $$id?: string };
101
+ if (ref.$$typeof === CLIENT_REFERENCE || ref.$$id) return;
102
+ RSDServer.registerClientReference(value, id, exportName);
103
+ }
104
+
105
+ /** Register `{ name: Component }` entries, keyed by name (id === name). */
106
+ export function registerClientComponents(
107
+ components: Record<string, unknown>,
108
+ ): void {
109
+ for (const [name, value] of Object.entries(components)) {
110
+ registerOne(value, name, name);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * A client manifest that resolves ANY registered client reference without
116
+ * needing to enumerate them. `$$id` is `${id}#${exportName}`; the serializer
117
+ * looks it up here. We omit `async`, so each `I` row is a 3-element row and the
118
+ * deserialized boundary's payload is a clean `resolved_module` whose value is
119
+ * `[id, [], name]` — synchronously readable by {@link findClientBoundaries}.
120
+ */
121
+ export function makeClientManifest(): unknown {
122
+ return new Proxy(
123
+ {},
124
+ {
125
+ get(_target, key) {
126
+ if (typeof key !== "string") return undefined;
127
+ const hash = key.lastIndexOf("#");
128
+ const id = hash >= 0 ? key.slice(0, hash) : key;
129
+ const name = hash >= 0 ? key.slice(hash + 1) : "default";
130
+ return { id, chunks: [], name };
131
+ },
132
+ },
133
+ );
134
+ }
135
+
136
+ let loadInstalled = false;
137
+ /**
138
+ * Install the deserialize-side module loader. For the non-async manifest above,
139
+ * the deserializer never calls it (boundaries stay placeholders), so it throws a
140
+ * clear error if a code path ever does try to execute a client reference.
141
+ */
142
+ function installDeserializeLoad(): void {
143
+ if (loadInstalled) return;
144
+ loadInstalled = true;
145
+ setRequireModule({
146
+ load: (id: string) => {
147
+ throw new Error(
148
+ `renderServerTree does not execute client references (deserialize-only). ` +
149
+ `A client reference "${id}" was resolved — render/interaction is the e2e tier.`,
150
+ );
151
+ },
152
+ });
153
+ }
154
+
155
+ function stringToStream(text: string): ReadableStream<Uint8Array> {
156
+ const bytes = new TextEncoder().encode(text);
157
+ return new ReadableStream({
158
+ start(controller) {
159
+ controller.enqueue(bytes);
160
+ controller.close();
161
+ },
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Serialize a server component to real Flight, then deserialize it back to an
167
+ * inspectable React element tree. See the module header for scope.
168
+ *
169
+ * Must run under the `react-server` export condition.
170
+ */
171
+ export async function renderServerTree(
172
+ element: ReactNode,
173
+ opts: RenderServerTreeOptions = {},
174
+ ): Promise<RenderServerTreeResult> {
175
+ if (opts.clientComponents) registerClientComponents(opts.clientComponents);
176
+ const flight = await serializeToFlightString(
177
+ element,
178
+ opts,
179
+ makeClientManifest(),
180
+ );
181
+ return { flight, tree: await deserializeFlight(flight) };
182
+ }
183
+
184
+ /**
185
+ * Deserialize a Flight wire string back to an inspectable React element tree:
186
+ * `createFromReadableStream` (vendored client), then unwrap Rango's payload
187
+ * wrapper and resolve the top server chunk so the consumer gets their element,
188
+ * not a lazy. Reused by renderServerTree AND renderHandler. Client references
189
+ * stay as inert boundary markers ({@link findClientBoundaries} reads them).
190
+ */
191
+ export async function deserializeFlight(flight: string): Promise<unknown> {
192
+ installDeserializeLoad();
193
+ const payload = await createFromReadableStream(stringToStream(flight));
194
+ return resolveServerLazy(unwrapPayload(payload));
195
+ }
196
+
197
+ /**
198
+ * The serializer wraps the element in Rango's payload shape
199
+ * (`{ metadata: { segments: [{ component }] } }`) to mirror the real wire
200
+ * format. Return the consumer's own element tree, not that wrapper.
201
+ */
202
+ function unwrapPayload(payload: unknown): unknown {
203
+ const segment = (
204
+ payload as { metadata?: { segments?: Array<{ component?: unknown }> } }
205
+ )?.metadata?.segments?.[0];
206
+ return segment && "component" in segment ? segment.component : payload;
207
+ }
208
+
209
+ interface FlightLazy {
210
+ _payload: { status: string; value: unknown };
211
+ _init: (payload: unknown) => unknown;
212
+ }
213
+
214
+ function asFlightLazy(node: unknown): FlightLazy | undefined {
215
+ const candidate = node as Partial<FlightLazy> | null;
216
+ if (
217
+ candidate &&
218
+ typeof candidate === "object" &&
219
+ typeof candidate._init === "function" &&
220
+ candidate._payload &&
221
+ typeof (candidate._payload as { status?: unknown }).status === "string"
222
+ ) {
223
+ return candidate as FlightLazy;
224
+ }
225
+ return undefined;
226
+ }
227
+
228
+ /**
229
+ * An async server component serializes as a deferred chunk that deserializes to
230
+ * a lazy (`status: "resolved_model"`). Initialize it to the materialized element
231
+ * (synchronous for a fully-drained stream; never calls a client `load`). Client
232
+ * references (`status: "resolved_module"`) are left untouched — they are the
233
+ * boundary markers {@link findClientBoundaries} reads.
234
+ */
235
+ function resolveServerLazy(node: unknown): unknown {
236
+ let current = node;
237
+ for (let guard = 0; guard < 1000; guard++) {
238
+ const lazy = asFlightLazy(current);
239
+ if (!lazy || lazy._payload.status !== "resolved_model") return current;
240
+ try {
241
+ current = lazy._init(lazy._payload);
242
+ } catch {
243
+ return current;
244
+ }
245
+ }
246
+ return current;
247
+ }
248
+
249
+ interface ClientBoundaryElement {
250
+ type: { _payload: { status: string; value: unknown[] } };
251
+ props?: Record<string, unknown>;
252
+ }
253
+
254
+ function isClientBoundaryElement(node: unknown): node is ClientBoundaryElement {
255
+ const type = (node as { type?: unknown })?.type as
256
+ | { _payload?: { status?: string; value?: unknown } }
257
+ | undefined;
258
+ const payload = type?._payload;
259
+ return (
260
+ !!payload &&
261
+ payload.status === "resolved_module" &&
262
+ Array.isArray(payload.value)
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Walk a deserialized tree and return every client boundary, in document order,
268
+ * each with its id, export name, and typed props. Pass `name` to keep only the
269
+ * boundaries with that export name.
270
+ *
271
+ * Always returns an array (no throw on zero/many). For a single expected
272
+ * boundary, destructure the first: `const [tag] = findClientBoundaries(tree,
273
+ * "PriceTag")` — and assert on `.length` when the count matters (a missing name
274
+ * yields `[]`, so `tag` would be `undefined`).
275
+ */
276
+ export function findClientBoundaries(
277
+ tree: unknown,
278
+ name?: string,
279
+ ): ClientBoundary[] {
280
+ const out: ClientBoundary[] = [];
281
+ const seen = new Set<unknown>();
282
+ const visit = (raw: unknown): void => {
283
+ // Materialize async-server-component chunks so we can traverse into them;
284
+ // client-reference chunks pass through untouched.
285
+ const node = resolveServerLazy(raw);
286
+ if (node == null || typeof node !== "object") return;
287
+ if (seen.has(node)) return;
288
+ seen.add(node);
289
+ if (Array.isArray(node)) {
290
+ for (const child of node) visit(child);
291
+ return;
292
+ }
293
+ if (isClientBoundaryElement(node)) {
294
+ const value = node.type._payload.value;
295
+ out.push({
296
+ id: String(value[0]),
297
+ name: String(value[2]),
298
+ props: node.props ?? {},
299
+ element: node,
300
+ });
301
+ }
302
+ // Recurse all own enumerable values: the tree is a deserialized payload
303
+ // (metadata -> segments -> component -> children/props), not just nested
304
+ // React props.
305
+ for (const value of Object.values(node as Record<string, unknown>)) {
306
+ visit(value);
307
+ }
308
+ };
309
+ visit(tree);
310
+ return name === undefined
311
+ ? out
312
+ : out.filter((boundary) => boundary.name === name);
313
+ }
314
+
315
+ /**
316
+ * Smoke check that the vendored client deserializer subpaths still resolve. The
317
+ * paths are private to plugin-rsc; a minor bump could relocate them. Call this in
318
+ * a test to fail loudly with a clear message instead of an opaque import error.
319
+ */
320
+ export function assertFlightTreeRuntimeAvailable(): void {
321
+ if (
322
+ typeof createFromReadableStream !== "function" ||
323
+ typeof setRequireModule !== "function" ||
324
+ typeof RSDServer.registerClientReference !== "function"
325
+ ) {
326
+ throw new Error(
327
+ "renderServerTree runtime not available: a @vitejs/plugin-rsc client/server " +
328
+ "subpath did not export the expected function. A plugin-rsc upgrade may have " +
329
+ "moved react/browser, core/browser, or vendor/react-server-dom/server.edge.",
330
+ );
331
+ }
332
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @rangojs/router/testing/flight
3
+ *
4
+ * Real React Server Component (Flight) rendering for unit tests. This entry is
5
+ * SEPARATE from the main ./testing barrel because its serializer (the vendored
6
+ * react-server-dom build) can only be imported under the `react-server` node
7
+ * condition; importing it elsewhere throws. Use it only from a Vitest project
8
+ * configured with that condition (see vitest.rsc.config.ts) — name those test
9
+ * files `*.rsc-test.{ts,tsx}` and run `pnpm test:unit:rsc`.
10
+ *
11
+ * This entry deliberately does NOT pull in Vitest. The `toMatchFlight` /
12
+ * `toMatchFlightSnapshot` matchers (which import `vitest`) live at the separate
13
+ * `@rangojs/router/testing/flight-matchers` subpath, so a consumer can import
14
+ * `renderToFlightString` without taking a hard dependency on Vitest.
15
+ *
16
+ * `renderToFlightString` returns the wire STRING (for `toMatchFlight`).
17
+ * `renderServerTree` additionally deserializes it back to an inspectable React
18
+ * element tree, so you can assert typed prop fidelity across the client boundary
19
+ * (a `Date` comes back a `Date`) and detect inlined-vs-island. Serialize +
20
+ * deserialize only — no hydration/interaction (that is the e2e tier).
21
+ */
22
+
23
+ export {
24
+ renderToFlightString,
25
+ normalizeFlight,
26
+ assertFlightRuntimeAvailable,
27
+ } from "./flight.js";
28
+ export type { RenderToFlightStringOptions } from "./flight.js";
29
+
30
+ export {
31
+ renderServerTree,
32
+ findClientBoundaries,
33
+ assertFlightTreeRuntimeAvailable,
34
+ } from "./flight-tree.js";
35
+ export type {
36
+ RenderServerTreeOptions,
37
+ RenderServerTreeResult,
38
+ ClientBoundary,
39
+ } from "./flight-tree.js";
40
+
41
+ export { renderHandler } from "./render-handler.js";
42
+ export type {
43
+ TestableHandler,
44
+ RenderHandlerOptions,
45
+ RenderHandlerResult,
46
+ } from "./render-handler.js";
@@ -0,0 +1,224 @@
1
+ /// <reference path="./flight-runtime.d.ts" />
2
+ /**
3
+ * renderToFlightString — REAL React Server Component (Flight) rendering for
4
+ * unit tests of @rangojs/router consumer apps.
5
+ *
6
+ * This module renders a server component tree to its Flight wire string using
7
+ * the same react-server-dom serializer the router uses at runtime. It runs in
8
+ * plain node (no Vite, no browser), but ONLY under the `react-server` export
9
+ * condition. The serializer is the VENDORED build shipped with
10
+ * @vitejs/plugin-rsc — the public `@vitejs/plugin-rsc/rsc` entry top-level
11
+ * imports Vite virtual modules and is not usable outside a Vite build.
12
+ *
13
+ * Run the example/tests for this module via the dedicated rsc vitest project
14
+ * (vitest.rsc.config.ts), which forces `--conditions=react-server` on the
15
+ * worker. The main vitest project must NOT use that condition (it would flip
16
+ * React to the no-hooks server build and break the ~50 tests that mock
17
+ * @vitejs/plugin-rsc/rsc).
18
+ *
19
+ * Scope / limitations (v1):
20
+ * - Server-only / leaf trees. A tree containing a CLIENT component emits an
21
+ * `I[...]` import row whose module id will not resolve against the empty `{}`
22
+ * client manifest used here — fine for snapshotting the SHAPE of the payload,
23
+ * but the client reference cannot be executed/hydrated. The interactive DOM
24
+ * render (`renderServer`) is deferred (see module TODO at bottom of report).
25
+ * - The vendored subpath is a private plugin-rsc path; a minor bump could move
26
+ * it. `assertFlightRuntimeAvailable()` provides a smoke check.
27
+ * - For stable snapshots, run under NODE_ENV=production: the production
28
+ * serializer drops the dev-only debug-info rows (the `N<timestamp>` reference
29
+ * row, the per-component `stack`/`env` rows, and `D{...}` timing rows),
30
+ * leaving just the rendered tree row(s).
31
+ */
32
+
33
+ import type { ReactNode } from "react";
34
+ // Vendored react-server-dom serializer. Resolves via plugin-rsc's
35
+ // `"./*": "./dist/*.js"` export to
36
+ // dist/vendor/react-server-dom/server.edge.js. Only loadable under the
37
+ // `react-server` export condition.
38
+ import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
39
+ import {
40
+ createRequestContext,
41
+ runWithRequestContext,
42
+ setRequestContextParams,
43
+ } from "../server/request-context.js";
44
+ import { seedVariables, type VarsInit } from "./internal/seed-vars.js";
45
+ import type { RscPayload } from "../rsc/types.js";
46
+ import type { ResolvedSegment } from "../types.js";
47
+
48
+ /**
49
+ * Options for {@link renderToFlightString}.
50
+ */
51
+ export interface RenderToFlightStringOptions {
52
+ /** Request URL. Defaults to `http://localhost/`. */
53
+ url?: string;
54
+ /** Request headers (e.g. Cookie) visible to the server tree. */
55
+ headers?: HeadersInit;
56
+ /** Env / bindings exposed as `ctx.env`. Defaults to `{}`. */
57
+ env?: unknown;
58
+ /** Route params exposed via `ctx.params` and loader contexts. */
59
+ params?: Record<string, string>;
60
+ /** Matched route name (drives `ctx.routeName` and scoped reverse). */
61
+ routeName?: string;
62
+ /**
63
+ * Context variables visible to the rendered tree via `ctx.get(...)` — as a
64
+ * prior middleware would have set them. Seeds the SAME way the handler-test
65
+ * primitives (`runInRequestContext`/`runLoader`) do, so a server component
66
+ * that reads `getRequestContext().get(MyVar)` during render is testable.
67
+ * Object form (`{ user }`) or `[key, value]` tuples (`[[userVar, u]]`).
68
+ */
69
+ vars?: VarsInit;
70
+ }
71
+
72
+ const DEFAULT_URL = "http://localhost/";
73
+
74
+ /**
75
+ * Wrap a single element in the minimal ResolvedSegment + RscPayload shape that
76
+ * mirrors Rango's wire format, so the serialized output matches what a real
77
+ * route segment would emit.
78
+ */
79
+ function wrapAsPayload(element: ReactNode, pathname: string): RscPayload {
80
+ const segment: ResolvedSegment = {
81
+ id: "test",
82
+ namespace: "",
83
+ type: "route",
84
+ index: 0,
85
+ component: element,
86
+ };
87
+ return {
88
+ metadata: {
89
+ pathname,
90
+ segments: [segment],
91
+ version: "test",
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Render a server component (or any ReactNode) to its Flight wire string.
98
+ *
99
+ * The element is wrapped in a minimal Rango segment + payload, then serialized
100
+ * with the vendored react-server-dom server. A request context is active for
101
+ * the duration of the render (drained INSIDE runWithRequestContext) so async
102
+ * server components can call getRequestContext(), read params, cookies, etc.
103
+ *
104
+ * Must run under the `react-server` export condition (see module header).
105
+ */
106
+ export async function renderToFlightString(
107
+ element: ReactNode,
108
+ opts: RenderToFlightStringOptions = {},
109
+ ): Promise<string> {
110
+ // Server-only trees: empty client manifest. A client reference would emit an
111
+ // unresolvable `I` row here; use renderServerTree (flight-tree.ts) when the
112
+ // tree has client boundaries you want to inspect.
113
+ return serializeToFlightString(element, opts, {});
114
+ }
115
+
116
+ /**
117
+ * Shared serialize core: set up a request context, wrap the element as a Rango
118
+ * payload, and serialize it with the given client-reference manifest. Used by
119
+ * {@link renderToFlightString} (empty manifest) and renderServerTree (a manifest
120
+ * that resolves every registered client reference).
121
+ *
122
+ * Must run under the `react-server` export condition (see module header).
123
+ */
124
+ export async function serializeToFlightString(
125
+ element: ReactNode,
126
+ opts: RenderToFlightStringOptions,
127
+ clientManifest: unknown,
128
+ ): Promise<string> {
129
+ const url = new URL(opts.url ?? DEFAULT_URL);
130
+ const request = new Request(url, { headers: opts.headers });
131
+ const ctx = createRequestContext({
132
+ env: opts.env ?? {},
133
+ request,
134
+ url,
135
+ // Seed vars so a server component reading ctx.get(MyVar) during render sees
136
+ // them — same seeding the handler-test primitives use.
137
+ variables: seedVariables({}, opts.vars),
138
+ });
139
+
140
+ return runWithRequestContext(ctx, () => {
141
+ setRequestContextParams(opts.params ?? {}, opts.routeName);
142
+ return serializeNodeToFlight(element, clientManifest, url.pathname);
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Serialize a node to a Flight string, ASSUMING a request context is already
148
+ * active (i.e. called inside `runWithRequestContext`). This is the core
149
+ * `renderHandler` reuses: it enters its own context, builds a HandlerContext,
150
+ * invokes the handler, then serializes the returned RSC in that SAME context (so
151
+ * cookies/headers/vars/handles the handler set are all on one context).
152
+ *
153
+ * Must run under the `react-server` export condition (see module header).
154
+ */
155
+ export async function serializeNodeToFlight(
156
+ node: ReactNode,
157
+ clientManifest: unknown,
158
+ pathname: string,
159
+ ): Promise<string> {
160
+ const payload = wrapAsPayload(node, pathname);
161
+ // Capture (do NOT rethrow) the first render error. The serializer calls
162
+ // onError from its own scheduled work; throwing there escapes as an unhandled
163
+ // rejection AND leaves the stream un-closed, so the drain below would hang
164
+ // until the test times out. Production's onError returns void (rsc-rendering.ts)
165
+ // so the stream completes with an error row. We mirror that — let the stream
166
+ // finish — then surface the error as a clean rejection after draining, so
167
+ // `await expect(...).rejects.toThrow()` works.
168
+ let renderError: unknown;
169
+ let didError = false;
170
+ const stream = RSDServer.renderToReadableStream(payload, clientManifest, {
171
+ onError(error: unknown) {
172
+ if (!didError) {
173
+ didError = true;
174
+ renderError = error;
175
+ }
176
+ },
177
+ });
178
+ // Drain inside the context so async components see ctx during streaming.
179
+ const text = await new Response(stream).text();
180
+ if (didError) throw renderError;
181
+ return text;
182
+ }
183
+
184
+ // Volatile leading reference row: `:N<timestamp>` (dev debug-info anchor).
185
+ const REFERENCE_ROW_RE = /^:N[\d.]+\n/;
186
+ // Absolute file:// paths embedded in dev STACK rows. The serializer emits stack
187
+ // frames as `["Component","file:///abs/path.tsx",<line>,<col>,...]`, so the
188
+ // path is a quoted JSON string immediately followed by `",<line>,<col>`. The
189
+ // lookahead scopes the scrub to exactly that frame shape, leaving a legitimate
190
+ // `file://` href in RENDERED content (e.g. `{"href":"file:///x"}`) untouched.
191
+ const FILE_URL_RE = /file:\/\/[^"\\]+(?=",\d+,\d+)/g;
192
+
193
+ /**
194
+ * Scrub volatile bits from a Flight string so snapshots are stable across runs
195
+ * and machines:
196
+ * - the leading `:N<timestamp>` reference row (dev only),
197
+ * - absolute `file://...` paths inside dev stack rows.
198
+ *
199
+ * Under NODE_ENV=production these rows are already absent; normalize is a
200
+ * no-op safety net there. In dev mode it removes the machine/clock-specific
201
+ * noise while leaving the rendered tree intact.
202
+ */
203
+ export function normalizeFlight(flight: string): string {
204
+ return flight
205
+ .replace(REFERENCE_ROW_RE, "")
206
+ .replace(FILE_URL_RE, "file://<path>");
207
+ }
208
+
209
+ /**
210
+ * Smoke check that the vendored serializer subpath still resolves and exposes
211
+ * `renderToReadableStream`. The vendored path is private to plugin-rsc; a minor
212
+ * bump could relocate it. Call this in a test to fail loudly with a clear
213
+ * message instead of an opaque import error.
214
+ */
215
+ export function assertFlightRuntimeAvailable(): void {
216
+ if (typeof RSDServer.renderToReadableStream !== "function") {
217
+ throw new Error(
218
+ "Vendored react-server-dom serializer not available: " +
219
+ "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge did not export " +
220
+ "renderToReadableStream. The private vendored subpath may have moved in " +
221
+ "a plugin-rsc upgrade.",
222
+ );
223
+ }
224
+ }