@rangojs/router 0.0.0-experimental.124 → 0.0.0-experimental.126

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 (235) hide show
  1. package/README.md +6 -4
  2. package/dist/bin/rango.js +3 -4
  3. package/dist/vite/index.js +315 -68
  4. package/package.json +19 -18
  5. package/skills/breadcrumbs/SKILL.md +60 -0
  6. package/skills/hooks/SKILL.md +2 -2
  7. package/skills/route/SKILL.md +6 -0
  8. package/skills/server-actions/SKILL.md +25 -1
  9. package/skills/testing/SKILL.md +17 -17
  10. package/skills/testing/cache-prerender.md +29 -3
  11. package/skills/testing/flight.md +13 -10
  12. package/skills/testing/render-handler.md +3 -0
  13. package/skills/testing/server-tree.md +1 -1
  14. package/skills/testing/setup.md +1 -1
  15. package/src/__internal.ts +0 -65
  16. package/src/browser/action-coordinator.ts +1 -1
  17. package/src/browser/action-fence.ts +10 -0
  18. package/src/browser/event-controller.ts +1 -83
  19. package/src/browser/navigation-store-handle.ts +3 -4
  20. package/src/browser/navigation-store.ts +0 -39
  21. package/src/browser/navigation-transaction.ts +0 -32
  22. package/src/browser/partial-update.ts +23 -84
  23. package/src/browser/prefetch/cache.ts +6 -45
  24. package/src/browser/prefetch/queue.ts +6 -3
  25. package/src/browser/rango-state.ts +2 -23
  26. package/src/browser/react/Link.tsx +0 -2
  27. package/src/browser/react/NavigationProvider.tsx +2 -1
  28. package/src/browser/react/ScrollRestoration.tsx +10 -6
  29. package/src/browser/react/filter-segment-order.ts +0 -2
  30. package/src/browser/react/index.ts +0 -45
  31. package/src/browser/react/location-state-shared.ts +0 -13
  32. package/src/browser/react/location-state.ts +0 -1
  33. package/src/browser/react/use-action.ts +6 -15
  34. package/src/browser/react/use-handle.ts +0 -5
  35. package/src/browser/react/use-link-status.ts +0 -4
  36. package/src/browser/react/use-navigation.ts +0 -3
  37. package/src/browser/react/use-params.ts +0 -2
  38. package/src/browser/react/use-router.ts +2 -1
  39. package/src/browser/react/use-search-params.ts +0 -5
  40. package/src/browser/react/use-segments.ts +0 -13
  41. package/src/browser/rsc-router.tsx +10 -3
  42. package/src/browser/server-action-bridge.ts +51 -3
  43. package/src/browser/types.ts +23 -5
  44. package/src/browser/validate-redirect-origin.ts +43 -16
  45. package/src/build/index.ts +8 -9
  46. package/src/build/route-trie.ts +46 -11
  47. package/src/build/route-types/param-extraction.ts +6 -3
  48. package/src/build/route-types/router-processing.ts +0 -8
  49. package/src/cache/cache-policy.ts +0 -54
  50. package/src/cache/cache-runtime.ts +48 -24
  51. package/src/cache/cache-scope.ts +0 -27
  52. package/src/cache/cache-tag.ts +0 -37
  53. package/src/cache/cf/cf-cache-store.ts +72 -45
  54. package/src/cache/cf/index.ts +0 -24
  55. package/src/cache/document-cache.ts +10 -36
  56. package/src/cache/handle-snapshot.ts +0 -40
  57. package/src/cache/index.ts +0 -27
  58. package/src/cache/memory-segment-store.ts +0 -52
  59. package/src/cache/profile-registry.ts +6 -30
  60. package/src/cache/read-through-swr.ts +41 -11
  61. package/src/cache/segment-codec.ts +0 -16
  62. package/src/cache/types.ts +0 -98
  63. package/src/client.rsc.tsx +4 -22
  64. package/src/client.tsx +19 -32
  65. package/src/context-var.ts +12 -0
  66. package/src/defer.ts +196 -0
  67. package/src/deps/ssr.ts +0 -1
  68. package/src/handle.ts +2 -12
  69. package/src/handles/MetaTags.tsx +0 -14
  70. package/src/handles/breadcrumbs.ts +16 -5
  71. package/src/handles/meta.ts +0 -39
  72. package/src/host/cookie-handler.ts +0 -36
  73. package/src/host/errors.ts +0 -24
  74. package/src/host/index.ts +6 -0
  75. package/src/host/pattern-matcher.ts +7 -50
  76. package/src/host/router.ts +1 -65
  77. package/src/host/testing.ts +0 -16
  78. package/src/host/types.ts +6 -2
  79. package/src/href-client.ts +0 -4
  80. package/src/index.rsc.ts +27 -2
  81. package/src/index.ts +7 -0
  82. package/src/internal-debug.ts +2 -4
  83. package/src/loader.rsc.ts +4 -15
  84. package/src/loader.ts +3 -9
  85. package/src/network-error-thrower.tsx +1 -6
  86. package/src/outlet-provider.tsx +1 -5
  87. package/src/prerender/param-hash.ts +10 -11
  88. package/src/prerender/store.ts +23 -30
  89. package/src/prerender.ts +34 -0
  90. package/src/redirect-origin.ts +100 -0
  91. package/src/root-error-boundary.tsx +1 -19
  92. package/src/route-content-wrapper.tsx +1 -44
  93. package/src/route-definition/dsl-helpers.ts +7 -19
  94. package/src/route-definition/helpers-types.ts +3 -3
  95. package/src/route-definition/redirect.ts +43 -9
  96. package/src/route-definition/resolve-handler-use.ts +6 -0
  97. package/src/route-map-builder.ts +0 -16
  98. package/src/router/content-negotiation.ts +0 -13
  99. package/src/router/error-handling.ts +12 -16
  100. package/src/router/find-match.ts +4 -31
  101. package/src/router/intercept-resolution.ts +10 -1
  102. package/src/router/lazy-includes.ts +1 -57
  103. package/src/router/loader-resolution.ts +25 -23
  104. package/src/router/logging.ts +0 -6
  105. package/src/router/manifest.ts +1 -25
  106. package/src/router/match-api.ts +0 -20
  107. package/src/router/match-context.ts +0 -22
  108. package/src/router/match-handlers.ts +0 -43
  109. package/src/router/match-middleware/background-revalidation.ts +0 -7
  110. package/src/router/match-middleware/cache-lookup.ts +96 -179
  111. package/src/router/match-middleware/cache-store.ts +0 -31
  112. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  113. package/src/router/match-middleware/segment-resolution.ts +0 -22
  114. package/src/router/match-pipelines.ts +1 -42
  115. package/src/router/match-result.ts +1 -52
  116. package/src/router/metrics.ts +0 -34
  117. package/src/router/middleware-types.ts +0 -116
  118. package/src/router/middleware.ts +77 -60
  119. package/src/router/navigation-snapshot.ts +0 -51
  120. package/src/router/params-util.ts +23 -0
  121. package/src/router/pattern-matching.ts +5 -56
  122. package/src/router/prerender-match.ts +56 -51
  123. package/src/router/request-classification.ts +1 -38
  124. package/src/router/revalidation.ts +14 -62
  125. package/src/router/route-snapshot.ts +0 -1
  126. package/src/router/router-context.ts +0 -27
  127. package/src/router/router-interfaces.ts +10 -0
  128. package/src/router/segment-resolution/fresh.ts +25 -57
  129. package/src/router/segment-resolution/helpers.ts +34 -0
  130. package/src/router/segment-resolution/loader-cache.ts +35 -23
  131. package/src/router/segment-resolution/revalidation.ts +188 -283
  132. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  133. package/src/router/segment-resolution.ts +4 -1
  134. package/src/router/segment-wrappers.ts +0 -3
  135. package/src/router/telemetry-otel.ts +0 -20
  136. package/src/router/telemetry.ts +0 -22
  137. package/src/router/timeout.ts +0 -20
  138. package/src/router/trie-matching.ts +66 -45
  139. package/src/router/types.ts +1 -63
  140. package/src/router/url-params.ts +0 -5
  141. package/src/router.ts +8 -11
  142. package/src/rsc/handler-context.ts +1 -0
  143. package/src/rsc/handler.ts +20 -4
  144. package/src/rsc/helpers.ts +71 -3
  145. package/src/rsc/json-route-result.ts +38 -0
  146. package/src/rsc/origin-guard.ts +9 -15
  147. package/src/rsc/progressive-enhancement.ts +10 -1
  148. package/src/rsc/redirect-guard.ts +99 -0
  149. package/src/rsc/response-route-handler.ts +23 -18
  150. package/src/rsc/rsc-rendering.ts +2 -7
  151. package/src/rsc/runtime-warnings.ts +14 -0
  152. package/src/rsc/server-action.ts +34 -29
  153. package/src/rsc/types.ts +6 -3
  154. package/src/search-params.ts +0 -16
  155. package/src/segment-loader-promise.ts +14 -2
  156. package/src/segment-system.tsx +79 -88
  157. package/src/server/handle-store.ts +7 -24
  158. package/src/server/loader-registry.ts +5 -24
  159. package/src/server/request-context.ts +29 -92
  160. package/src/ssr/index.tsx +14 -14
  161. package/src/static-handler.ts +2 -27
  162. package/src/testing/cache-status.ts +44 -48
  163. package/src/testing/collect-handle.ts +1 -24
  164. package/src/testing/dispatch.ts +43 -6
  165. package/src/testing/e2e/index.ts +1 -22
  166. package/src/testing/e2e/matchers.ts +0 -16
  167. package/src/testing/flight-matchers.ts +0 -13
  168. package/src/testing/flight-normalize.ts +3 -30
  169. package/src/testing/flight.ts +46 -48
  170. package/src/testing/generated-routes.ts +1 -41
  171. package/src/testing/index.ts +1 -21
  172. package/src/testing/internal/context.ts +3 -45
  173. package/src/testing/internal/seed-vars.ts +0 -26
  174. package/src/testing/render-handler.ts +31 -61
  175. package/src/testing/render-route.tsx +75 -103
  176. package/src/testing/run-loader.ts +0 -96
  177. package/src/testing/run-middleware.ts +0 -26
  178. package/src/theme/ThemeProvider.tsx +0 -52
  179. package/src/theme/ThemeScript.tsx +0 -6
  180. package/src/theme/constants.ts +0 -12
  181. package/src/theme/index.ts +0 -7
  182. package/src/theme/theme-context.ts +1 -5
  183. package/src/theme/theme-script.ts +0 -14
  184. package/src/theme/use-theme.ts +0 -3
  185. package/src/types/boundaries.ts +0 -35
  186. package/src/types/error-types.ts +25 -89
  187. package/src/types/global-namespace.ts +4 -14
  188. package/src/types/handler-context.ts +28 -9
  189. package/src/types/index.ts +0 -10
  190. package/src/types/request-scope.ts +0 -19
  191. package/src/types/route-config.ts +6 -50
  192. package/src/types/route-entry.ts +0 -6
  193. package/src/types/segments.ts +0 -13
  194. package/src/urls/include-helper.ts +0 -4
  195. package/src/urls/index.ts +0 -6
  196. package/src/urls/path-helper-types.ts +2 -2
  197. package/src/urls/path-helper.ts +0 -54
  198. package/src/urls/urls-function.ts +0 -13
  199. package/src/use-loader.tsx +0 -186
  200. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  201. package/src/vite/discovery/discover-routers.ts +28 -18
  202. package/src/vite/discovery/prerender-collection.ts +2 -4
  203. package/src/vite/discovery/state.ts +5 -0
  204. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  205. package/src/vite/plugin-types.ts +35 -9
  206. package/src/vite/plugins/cjs-to-esm.ts +0 -11
  207. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  208. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  209. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  210. package/src/vite/plugins/expose-action-id.ts +2 -73
  211. package/src/vite/plugins/expose-id-utils.ts +0 -55
  212. package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
  213. package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
  214. package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
  215. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  216. package/src/vite/plugins/expose-internal-ids.ts +10 -0
  217. package/src/vite/plugins/performance-tracks.ts +0 -3
  218. package/src/vite/plugins/refresh-cmd.ts +1 -1
  219. package/src/vite/plugins/use-cache-transform.ts +21 -46
  220. package/src/vite/plugins/version-injector.ts +0 -20
  221. package/src/vite/plugins/version-plugin.ts +1 -49
  222. package/src/vite/plugins/virtual-entries.ts +0 -15
  223. package/src/vite/rango.ts +2 -108
  224. package/src/vite/router-discovery.ts +9 -1
  225. package/src/vite/utils/ast-handler-extract.ts +0 -16
  226. package/src/vite/utils/bundle-analysis.ts +6 -13
  227. package/src/vite/utils/client-chunks.ts +0 -6
  228. package/src/vite/utils/forward-user-plugins.ts +0 -22
  229. package/src/vite/utils/manifest-utils.ts +0 -4
  230. package/src/vite/utils/package-resolution.ts +1 -73
  231. package/src/vite/utils/prerender-utils.ts +0 -35
  232. package/src/vite/utils/shared-utils.ts +3 -35
  233. package/src/browser/shallow.ts +0 -40
  234. package/src/handles/index.ts +0 -7
  235. package/src/router/middleware-cookies.ts +0 -55
@@ -37,6 +37,10 @@
37
37
  * (?_rsc_partial / ?_rsc_action): converted to a 204 + X-RSC-Redirect via the
38
38
  * real interceptRedirectForPartial, so fetch() does not auto-follow the 3xx —
39
39
  * identical to production's no-location-state path.
40
+ * - The open-redirect guard (rsc/redirect-guard.ts) on full (browser-followed)
41
+ * redirects: a cross-origin Location is rewritten to the basename root unless
42
+ * redirect(url, { external: true }) opted out, mirroring production's single
43
+ * handler chokepoint. Soft partial/action redirects are 204 and pass through.
40
44
  *
41
45
  * What dispatch DOES NOT support (and why):
42
46
  * - RSC component routes — rendering requires the Flight serializer + React
@@ -91,6 +95,13 @@ import {
91
95
  interceptRedirectForPartial,
92
96
  mergeStubHeadersAndFinalize,
93
97
  } from "../rsc/helpers.js";
98
+ import { guardOutgoingRedirect } from "../rsc/redirect-guard.js";
99
+ import { stringifyJsonRouteResult } from "../rsc/json-route-result.js";
100
+ import {
101
+ EXTERNAL_REDIRECT_MARKER,
102
+ isExternalRedirect,
103
+ markExternalRedirect,
104
+ } from "../redirect-origin.js";
94
105
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
95
106
  import type { Rango } from "../router/router-interfaces.js";
96
107
 
@@ -154,7 +165,8 @@ function toRequest(request: Request | string): Request {
154
165
  /**
155
166
  * Serialize a NON-Response response-route handler result, mirroring the
156
167
  * router's handleResponseRoute() contract:
157
- * - "json" serializes the value verbatim (bare) with application/json,
168
+ * - "json" serializes the value (bare) with application/json, rejecting a nested
169
+ * unresolved Promise via the shared stringifyJsonRouteResult guard,
158
170
  * - text/html/xml/md stringify with the mapped MIME type.
159
171
  *
160
172
  * A handler-returned Response is NOT routed here — callHandler re-wraps it via
@@ -167,7 +179,12 @@ function serializeResponseRouteResult(
167
179
  responseType: string,
168
180
  ): Response {
169
181
  if (responseType === "json") {
170
- return new Response(JSON.stringify(result), {
182
+ // Serialize through the SAME guard production uses: a nested unresolved
183
+ // Promise (forgotten await) throws RESPONSE_NOT_SERIALIZABLE here, caught by
184
+ // callHandler's catch and mapped to the identical typed 500 production
185
+ // returns -- so a dispatch json test fails exactly where production would,
186
+ // instead of silently emitting {} and passing.
187
+ return new Response(stringifyJsonRouteResult(result), {
171
188
  status: 200,
172
189
  headers: { "content-type": "application/json;charset=utf-8" },
173
190
  });
@@ -252,16 +269,27 @@ function rewrapHandlerResponse(result: Response): Response {
252
269
  }
253
270
  const headers = new Headers();
254
271
  result.headers.forEach((value, key) => {
272
+ // Mirror production: never copy the reserved external-redirect marker off a
273
+ // handler result (it is not a trust signal; the opt-in is the out-of-band
274
+ // brand transferred below).
275
+ if (key.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
255
276
  if (key.toLowerCase() === "set-cookie") {
256
277
  headers.append(key, value);
257
278
  } else {
258
279
  headers.set(key, value);
259
280
  }
260
281
  });
261
- return createResponseWithMergedHeaders(result.body, {
282
+ const rewrapped = createResponseWithMergedHeaders(result.body, {
262
283
  status: result.status,
263
284
  headers,
264
285
  });
286
+ // Mirror production's rewrapResponse: transfer the out-of-band external brand
287
+ // only from a genuinely branded result (a real redirect(url, { external:
288
+ // true })), never from a proxied upstream's forged header.
289
+ if (isExternalRedirect(result)) {
290
+ markExternalRedirect(rewrapped);
291
+ }
292
+ return rewrapped;
265
293
  }
266
294
 
267
295
  /**
@@ -506,7 +534,6 @@ export async function dispatch<TEnv = any>(
506
534
  regex: null,
507
535
  paramNames: [],
508
536
  handler: mw.handler,
509
- mountPrefix: null,
510
537
  } as MiddlewareEntry<TEnv>,
511
538
  params: mw.params,
512
539
  }),
@@ -569,13 +596,23 @@ export async function dispatch<TEnv = any>(
569
596
  // callbacks via finalizeResponse. dispatch is RSC-free, so the
570
597
  // createRedirectFlightResponse stand-in falls back to the no-state
571
598
  // 204 + X-RSC-Redirect (see the location-state divergence in the header).
599
+ let finalResponse: Response;
572
600
  if (isPartial || isAction) {
573
601
  const intercepted = interceptRedirectForPartial(
574
602
  mwResponse,
575
603
  (redirectUrl) => createSimpleRedirectResponse(redirectUrl),
576
604
  );
577
- return finalizeResponse(intercepted ?? mwResponse);
605
+ finalResponse = finalizeResponse(intercepted ?? mwResponse);
606
+ } else {
607
+ finalResponse = finalizeResponse(mwResponse);
578
608
  }
579
- return finalizeResponse(mwResponse);
609
+
610
+ // Mirror production's single open-redirect chokepoint (handler.ts): every
611
+ // browser-followed (3xx + Location) redirect is same-origin guarded before
612
+ // it leaves -- a cross-origin Location is rewritten to the basename root
613
+ // unless redirect(url, { external: true }) opted out. Soft partial/action
614
+ // redirects are 204 + X-RSC-Redirect and pass through untouched (the client
615
+ // validates them), so this is a no-op for them.
616
+ return guardOutgoingRedirect(finalResponse, url.origin, router.basename);
580
617
  });
581
618
  }
@@ -1,8 +1,3 @@
1
- // Public entry for the consumer e2e harness. `createRangoE2E({ test, expect })`
2
- // wires the server fixture, page helpers, parity helpers, and matchers around
3
- // the consumer's Playwright `test`/`expect` objects so this module never
4
- // imports `@playwright/test` at runtime (type-only imports are erased).
5
-
6
1
  import type { Expect, TestType } from "@playwright/test";
7
2
  import {
8
3
  createUseFixture,
@@ -35,14 +30,9 @@ import {
35
30
  } from "./parity.js";
36
31
  import { createRangoMatchers, type RangoMatchers } from "./matchers.js";
37
32
 
38
- // Cache-status helpers are pure (cache-status.ts imports only TYPES), so they
39
- // are safe to surface from this Playwright-runnable entry. Importing them from
40
- // the `@rangojs/router/testing` barrel does NOT work in a plain Playwright
41
- // runner — the barrel transitively pulls the build-only `@rangojs/router:version`
42
- // virtual via the route-manifest path. Asserting cache status on a real
43
- // response is an e2e activity, so this is their Playwright-safe home.
44
33
  export {
45
34
  assertCacheStatus,
35
+ assertCacheDecision,
46
36
  parseCacheHeader,
47
37
  createCacheSink,
48
38
  filterCacheDecisions,
@@ -50,9 +40,6 @@ export {
50
40
  type ExpectedCacheStatus,
51
41
  type CacheStatusTarget,
52
42
  } from "../cache-status.js";
53
-
54
- // Re-export standalone helpers and all public types so the barrel can re-export
55
- // them from a single module.
56
43
  export {
57
44
  testId,
58
45
  waitForHydration,
@@ -87,7 +74,6 @@ export interface RangoE2E extends PageHelpers, Parity {
87
74
  useFixture: (options: FixtureOptions) => Fixture;
88
75
  testNoJs: TestType<any, any>;
89
76
  rangoMatchers: RangoMatchers;
90
- // Standalone helpers, re-surfaced for convenience.
91
77
  testId: typeof testId;
92
78
  waitForHydration: typeof waitForHydration;
93
79
  waitForNavigation: typeof waitForNavigation;
@@ -102,12 +88,6 @@ export interface RangoE2E extends PageHelpers, Parity {
102
88
  measureTime: typeof measureTime;
103
89
  }
104
90
 
105
- /**
106
- * Wire the full e2e harness around a consumer's Playwright `test`/`expect`.
107
- *
108
- * @param defaultRoot - fallback app root for `parityDescribe` when a call omits
109
- * `options.root`.
110
- */
111
91
  export function createRangoE2E({
112
92
  test,
113
93
  expect,
@@ -132,7 +112,6 @@ export function createRangoE2E({
132
112
  rangoMatchers,
133
113
  ...parity,
134
114
  ...pageHelpers,
135
- // Standalone helpers.
136
115
  testId,
137
116
  waitForHydration,
138
117
  waitForNavigation,
@@ -1,6 +1,3 @@
1
- // Custom Playwright matchers for Rango assertions. Returned as an object
2
- // suitable for `expect.extend(...)`. v1 ships only `toHaveRangoPathname`.
3
-
4
1
  import type { Expect, Page } from "@playwright/test";
5
2
 
6
3
  interface MatcherResult {
@@ -12,17 +9,6 @@ export interface RangoMatchers {
12
9
  toHaveRangoPathname: (page: Page, expected: string) => MatcherResult;
13
10
  }
14
11
 
15
- /**
16
- * Build the matcher object for `expect.extend(createRangoMatchers(expect))`.
17
- *
18
- * `toHaveRangoPathname(page, expected)` asserts that the pathname of the page's
19
- * current URL equals `expected`.
20
- *
21
- * TODO: `toHaveSegments` / `toHaveParams` are intentionally not implemented.
22
- * They require a client-emitted signal (the active segment chain / resolved
23
- * params exposed on the page) that does not exist yet; implementing them by
24
- * scraping the DOM would be a guess. Add them once the router emits that signal.
25
- */
26
12
  export function createRangoMatchers(_expect: Expect): RangoMatchers {
27
13
  return {
28
14
  toHaveRangoPathname(page: Page, expected: string): MatcherResult {
@@ -39,8 +25,6 @@ export function createRangoMatchers(_expect: Expect): RangoMatchers {
39
25
  };
40
26
  }
41
27
 
42
- // Type augmentation so consumers can call `await expect(page).toHaveRangoPathname("/x")`
43
- // after `expect.extend(rangoMatchers)`, without re-declaring the matcher.
44
28
  declare global {
45
29
  // eslint-disable-next-line @typescript-eslint/no-namespace
46
30
  namespace PlaywrightTest {
@@ -42,14 +42,6 @@ interface MatcherResult {
42
42
  message: () => string;
43
43
  }
44
44
 
45
- /**
46
- * Matcher object for `expect.extend(flightMatchers)`.
47
- *
48
- * - `toMatchFlight(received, expected)` — `received` is a rendered Flight
49
- * string; passes if its normalized form contains `expected`.
50
- * - `toMatchFlightSnapshot(received)` — delegates to vitest's snapshot on the
51
- * normalized Flight string.
52
- */
53
45
  export const flightMatchers: {
54
46
  toMatchFlight(received: string, expected: string): MatcherResult;
55
47
  toMatchFlightSnapshot(received: string): MatcherResult;
@@ -78,12 +70,7 @@ export const flightMatchers: {
78
70
  },
79
71
 
80
72
  toMatchFlightSnapshot(received: string): MatcherResult {
81
- // Delegate to vitest's snapshot engine on the normalized string. The
82
- // snapshot is keyed by the current test file/title (vitest tracks this via
83
- // the active test context), not by this call site, so delegating through a
84
- // freshly imported `expect` is reliable.
85
73
  expect(normalizeFlight(received)).toMatchSnapshot();
86
- // toMatchSnapshot throws on mismatch; reaching here means it passed.
87
74
  return {
88
75
  pass: true,
89
76
  message: () => "Flight snapshot matched.",
@@ -1,36 +1,9 @@
1
- /**
2
- * normalizeFlight — scrub volatile bits from a Flight wire string so snapshots
3
- * are stable across runs/machines.
4
- *
5
- * This is two regex replacements and NOTHING else. It is split out of flight.ts
6
- * on purpose: flight.ts top-level imports the vendored react-server-dom
7
- * serializer, which throws when imported outside the `react-server` export
8
- * condition. The flight-matchers module (and a consumer's shared `setupFiles`
9
- * that does `expect.extend(flightMatchers)`) must be importable under the PLAIN
10
- * node condition, so the normalizer it needs cannot live next to that import.
11
- * flight.ts re-exports normalizeFlight from here, so the public surface of the
12
- * `@rangojs/router/testing/flight` entry is unchanged.
13
- */
14
-
15
- // Volatile leading reference row: `:N<timestamp>` (dev debug-info anchor).
1
+ // Volatile leading reference row: `:N<timestamp>` (dev only).
16
2
  const REFERENCE_ROW_RE = /^:N[\d.]+\n/;
17
- // Absolute file:// paths embedded in dev STACK rows. The serializer emits stack
18
- // frames as `["Component","file:///abs/path.tsx",<line>,<col>,...]`, so the
19
- // path is a quoted JSON string immediately followed by `",<line>,<col>`. The
20
- // lookahead scopes the scrub to exactly that frame shape, leaving a legitimate
21
- // `file://` href in RENDERED content (e.g. `{"href":"file:///x"}`) untouched.
3
+ // Absolute file:// paths in dev stack rows. Pattern matches frames
4
+ // `["Component","file:///path",<line>,<col>...]` and scrubs the path only.
22
5
  const FILE_URL_RE = /file:\/\/[^"\\]+(?=",\d+,\d+)/g;
23
6
 
24
- /**
25
- * Scrub volatile bits from a Flight string so snapshots are stable across runs
26
- * and machines:
27
- * - the leading `:N<timestamp>` reference row (dev only),
28
- * - absolute `file://...` paths inside dev stack rows.
29
- *
30
- * Under NODE_ENV=production these rows are already absent; normalize is a
31
- * no-op safety net there. In dev mode it removes the machine/clock-specific
32
- * noise while leaving the rendered tree intact.
33
- */
34
7
  export function normalizeFlight(flight: string): string {
35
8
  return flight
36
9
  .replace(REFERENCE_ROW_RE, "")
@@ -70,6 +70,14 @@ export interface RenderToFlightStringOptions {
70
70
  params?: Record<string, string>;
71
71
  /** Matched route name (drives `ctx.routeName` and scoped reverse). */
72
72
  routeName?: string;
73
+ /**
74
+ * Route name -> pattern map enabling a SCOPED `ctx.reverse()` (like
75
+ * `renderHandler`). Without it, a server component that reverses resolves
76
+ * against the GLOBAL route map and is order-dependent on whatever router
77
+ * registered last. Pass the router-under-test's map to make reversing
78
+ * deterministic.
79
+ */
80
+ routeMap?: Record<string, string>;
73
81
  /**
74
82
  * Context variables visible to the rendered tree via `ctx.get(...)` — as a
75
83
  * prior middleware would have set them. Seeds the SAME way the handler-test
@@ -83,12 +91,43 @@ export interface RenderToFlightStringOptions {
83
91
  const DEFAULT_URL = "http://localhost/";
84
92
 
85
93
  /**
86
- * Guard the pre-rename `{ url }` option (renamed to `{ request }`). A plain-JS
87
- * consumer, or one whose object spread defeats the compile-time type error,
88
- * would otherwise have `{ url }` SILENTLY ignored and render against
89
- * http://localhost/. Throw a clear migration error instead. Cheap: a single
90
- * `in` check on the already-built options object.
94
+ * True when `error` is the out-of-react-server stub thrown by index.ts's
95
+ * server-only exports (getRequestContext/cookies/headers/...) i.e. the bare
96
+ * `@rangojs/router` specifier resolved to index.ts, not index.rsc.ts, because
97
+ * the rsc Vitest project is missing the `rangoTestAliases` alias. Matches both
98
+ * substrings of `serverOnlyStubError` (index.ts) so a normal app error cannot
99
+ * over-match. Shared with render-handler.ts so the two Flight primitives report
100
+ * the same misconfiguration identically.
101
+ */
102
+ export function isServerOnlyStubError(error: unknown): boolean {
103
+ return (
104
+ error instanceof Error &&
105
+ error.message.includes("is only available from") &&
106
+ error.message.includes("react-server")
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Rethrow a server tree render error. When it is the missing-rsc-alias stub
112
+ * (above), rethrow an actionable message naming `rangoTestAliases` instead of
113
+ * the opaque stub text; otherwise rethrow the original unchanged. Classify the
114
+ * ORIGINAL error before constructing the wrapper so the wrapper's `Original: ...`
115
+ * echo (which re-embeds the matched substrings) never re-triggers the predicate.
91
116
  */
117
+ function rethrowFlightRenderError(error: unknown): never {
118
+ if (isServerOnlyStubError(error)) {
119
+ throw new Error(
120
+ `The server component called a server-only API ` +
121
+ `(getRequestContext/cookies/headers/...) but "@rangojs/router" resolved to ` +
122
+ `the out-of-react-server stub. Add rangoTestAliases({ preset }) to your ` +
123
+ `vitest.rsc.config.ts \`resolve.alias\` so the bare specifier maps to ` +
124
+ `index.rsc.ts (the real react-server implementations). ` +
125
+ `Original: ${(error as Error).message}`,
126
+ );
127
+ }
128
+ throw error;
129
+ }
130
+
92
131
  export function assertNoLegacyUrlOption(opts: object, fnName: string): void {
93
132
  if ("url" in opts) {
94
133
  throw new Error(
@@ -100,11 +139,6 @@ export function assertNoLegacyUrlOption(opts: object, fnName: string): void {
100
139
  }
101
140
  }
102
141
 
103
- /**
104
- * Wrap a single element in the minimal ResolvedSegment + RscPayload shape that
105
- * mirrors Rango's wire format, so the serialized output matches what a real
106
- * route segment would emit.
107
- */
108
142
  function wrapAsPayload(element: ReactNode, pathname: string): RscPayload {
109
143
  const segment: ResolvedSegment = {
110
144
  id: "test",
@@ -137,20 +171,9 @@ export async function renderToFlightString(
137
171
  opts: RenderToFlightStringOptions = {},
138
172
  ): Promise<string> {
139
173
  assertNoLegacyUrlOption(opts, "renderToFlightString");
140
- // Server-only trees: empty client manifest. A client reference would emit an
141
- // unresolvable `I` row here; use renderServerTree (flight-tree.ts) when the
142
- // tree has client boundaries you want to inspect.
143
174
  return serializeToFlightString(element, opts, {});
144
175
  }
145
176
 
146
- /**
147
- * Shared serialize core: set up a request context, wrap the element as a Rango
148
- * payload, and serialize it with the given client-reference manifest. Used by
149
- * {@link renderToFlightString} (empty manifest) and renderServerTree (a manifest
150
- * that resolves every registered client reference).
151
- *
152
- * Must run under the `react-server` export condition (see module header).
153
- */
154
177
  export async function serializeToFlightString(
155
178
  element: ReactNode,
156
179
  opts: RenderToFlightStringOptions,
@@ -167,39 +190,21 @@ export async function serializeToFlightString(
167
190
  env: opts.env ?? {},
168
191
  request,
169
192
  url,
170
- // Seed vars so a server component reading ctx.get(MyVar) during render sees
171
- // them — same seeding the handler-test primitives use.
172
193
  variables: seedVariables({}, opts.vars),
173
194
  });
174
195
 
175
196
  return runWithRequestContext(ctx, () => {
176
- setRequestContextParams(opts.params ?? {}, opts.routeName);
197
+ setRequestContextParams(opts.params ?? {}, opts.routeName, opts.routeMap);
177
198
  return serializeNodeToFlight(element, clientManifest, url.pathname);
178
199
  });
179
200
  }
180
201
 
181
- /**
182
- * Serialize a node to a Flight string, ASSUMING a request context is already
183
- * active (i.e. called inside `runWithRequestContext`). This is the core
184
- * `renderHandler` reuses: it enters its own context, builds a HandlerContext,
185
- * invokes the handler, then serializes the returned RSC in that SAME context (so
186
- * cookies/headers/vars/handles the handler set are all on one context).
187
- *
188
- * Must run under the `react-server` export condition (see module header).
189
- */
190
202
  export async function serializeNodeToFlight(
191
203
  node: ReactNode,
192
204
  clientManifest: unknown,
193
205
  pathname: string,
194
206
  ): Promise<string> {
195
207
  const payload = wrapAsPayload(node, pathname);
196
- // Capture (do NOT rethrow) the first render error. The serializer calls
197
- // onError from its own scheduled work; throwing there escapes as an unhandled
198
- // rejection AND leaves the stream un-closed, so the drain below would hang
199
- // until the test times out. Production's onError returns void (rsc-rendering.ts)
200
- // so the stream completes with an error row. We mirror that — let the stream
201
- // finish — then surface the error as a clean rejection after draining, so
202
- // `await expect(...).rejects.toThrow()` works.
203
208
  let renderError: unknown;
204
209
  let didError = false;
205
210
  const stream = RSDServer.renderToReadableStream(payload, clientManifest, {
@@ -210,18 +215,11 @@ export async function serializeNodeToFlight(
210
215
  }
211
216
  },
212
217
  });
213
- // Drain inside the context so async components see ctx during streaming.
214
218
  const text = await new Response(stream).text();
215
- if (didError) throw renderError;
219
+ if (didError) rethrowFlightRenderError(renderError);
216
220
  return text;
217
221
  }
218
222
 
219
- /**
220
- * Smoke check that the vendored serializer subpath still resolves and exposes
221
- * `renderToReadableStream`. The vendored path is private to plugin-rsc; a minor
222
- * bump could relocate it. Call this in a test to fail loudly with a clear
223
- * message instead of an opaque import error.
224
- */
225
223
  export function assertFlightRuntimeAvailable(): void {
226
224
  if (typeof RSDServer.renderToReadableStream !== "function") {
227
225
  throw new Error(
@@ -32,14 +32,6 @@ interface RouterWithRouteMap {
32
32
  findMatch?: (pathname: string) => unknown;
33
33
  }
34
34
 
35
- /**
36
- * Derive a best-effort concrete path from a route pattern so `findMatch` can be
37
- * invoked to expand a lazy include. `:param`, `:param(constraint)`, optional
38
- * `:param?`, and `*` are all replaced with a literal segment. A constrained
39
- * param may not match its constraint (so that one route's match fails), but
40
- * since matching ANY route in an include expands ALL of the include's routes,
41
- * a sibling route in the same include will still trigger expansion.
42
- */
43
35
  function concretePath(pattern: string): string {
44
36
  return (
45
37
  pattern
@@ -50,17 +42,6 @@ function concretePath(pattern: string): string {
50
42
  );
51
43
  }
52
44
 
53
- /**
54
- * Force-expand the router's lazy `include()`d routes into `router.routeMap`.
55
- *
56
- * All Rango includes are lazy — their child routes only populate `routeMap` when
57
- * the router first matches a path inside them (in production the build-time
58
- * manifest virtual carries the full map; in a bare test that virtual is absent).
59
- * To make the whole-app drift check work in a unit test, we trigger expansion by
60
- * calling `findMatch` on a concrete path derived from each known pattern. This is
61
- * idempotent and side-effect-free beyond populating the route map. Routers that
62
- * don't expose `findMatch` (e.g. a plain `{ routeMap }` object) are left as-is.
63
- */
64
45
  function expandLazyIncludes(
65
46
  router: RouterWithRouteMap,
66
47
  patterns: Iterable<string>,
@@ -70,10 +51,7 @@ function expandLazyIncludes(
70
51
  for (const pattern of patterns) {
71
52
  try {
72
53
  findMatch.call(router, concretePath(pattern));
73
- } catch {
74
- // A pattern that fails to match (constrained param, etc.) is fine — a
75
- // sibling route in the same include still triggers expansion.
76
- }
54
+ } catch {}
77
55
  }
78
56
  }
79
57
 
@@ -100,11 +78,6 @@ export interface GeneratedRoutesDiff {
100
78
  ok: boolean;
101
79
  }
102
80
 
103
- /**
104
- * Normalize a route map value to its pattern string. Route maps may carry
105
- * either a bare pattern string or a `{ path, ... }` object (for response/search
106
- * routes); compare on the `path`.
107
- */
108
81
  function patternOf(value: unknown): string {
109
82
  if (typeof value === "string") return value;
110
83
  if (
@@ -118,19 +91,12 @@ function patternOf(value: unknown): string {
118
91
  return String(value);
119
92
  }
120
93
 
121
- /**
122
- * Compute the diff between a router's runtime route map and a generated map.
123
- */
124
94
  export function diffGeneratedRoutes(
125
95
  router: RouterWithRouteMap,
126
96
  generatedMap?: Record<string, unknown>,
127
97
  ): GeneratedRoutesDiff {
128
98
  const generated = generatedMap ?? getGlobalRouteMap();
129
99
 
130
- // Lazy `include()`d routes are absent from `routeMap` until first matched, so
131
- // expand them first (using the generated patterns to drive the matches) —
132
- // otherwise every included route is a false `missing`. No-op for plain
133
- // `{ routeMap }` objects that don't expose `findMatch`.
134
100
  expandLazyIncludes(
135
101
  router,
136
102
  Object.values(generated).map((v) => patternOf(v)),
@@ -155,12 +121,6 @@ export function diffGeneratedRoutes(
155
121
  }
156
122
 
157
123
  for (const name of Object.keys(runtime)) {
158
- // Auto-generated internal names ($path_*/$prefix_*) live in the runtime
159
- // mergedRouteMap but are deliberately excluded from the generated
160
- // *.named-routes.gen.ts file (route-types-writer / runtime-discovery skip
161
- // them). Reporting them as `extra` would throw on a perfectly in-sync app
162
- // that simply uses an unnamed path()/include() route, so skip them here to
163
- // match exactly the surface the generator emits.
164
124
  if (isAutoGeneratedRouteName(name)) continue;
165
125
  if (!(name in generated)) {
166
126
  extra.push(name);
@@ -34,7 +34,6 @@
34
34
  * - RSC: see @rangojs/router/testing/flight
35
35
  */
36
36
 
37
- // Unit
38
37
  export { runMiddleware } from "./run-middleware.js";
39
38
  export type {
40
39
  RunMiddlewareOptions,
@@ -48,17 +47,12 @@ export type {
48
47
  TestLoaderContext,
49
48
  } from "./run-loader.js";
50
49
 
51
- // Integration
52
50
  export { dispatch } from "./dispatch.js";
53
51
  export type { DispatchOptions } from "./dispatch.js";
54
52
 
55
- // renderRoute lives at `@rangojs/router/testing/dom` — it pulls React, the
56
- // browser runtime, and @testing-library/react types, which this barrel keeps
57
- // out so node-only unit suites depend on none of them.
58
-
59
- // Cross-cutting: cache/prerender status
60
53
  export {
61
54
  assertCacheStatus,
55
+ assertCacheDecision,
62
56
  parseCacheHeader,
63
57
  createCacheSink,
64
58
  filterCacheDecisions,
@@ -68,9 +62,6 @@ export type {
68
62
  CacheStatusTarget,
69
63
  CacheSink,
70
64
  } from "./cache-status.js";
71
- // The telemetry event types a cache-status assertion inspects (createCacheSink
72
- // records CacheDecisionEvents; filterCacheDecisions narrows them). Re-exported
73
- // here so a test can annotate the events without reaching past `@rangojs/router/testing`.
74
65
  export type {
75
66
  TelemetryEvent,
76
67
  TelemetrySink,
@@ -79,10 +70,8 @@ export type {
79
70
  CacheSegmentStatus,
80
71
  } from "../router/telemetry.js";
81
72
 
82
- // Cross-cutting: handle collect/accumulator
83
73
  export { collectHandle } from "./collect-handle.js";
84
74
 
85
- // Cross-cutting: generated-route drift
86
75
  export {
87
76
  diffGeneratedRoutes,
88
77
  assertGeneratedRoutesMatch,
@@ -92,7 +81,6 @@ export type {
92
81
  GeneratedRouteMismatch,
93
82
  } from "./generated-routes.js";
94
83
 
95
- // Advanced: build a real RequestContext for bespoke loader/middleware setups
96
84
  export {
97
85
  createTestRequestContext,
98
86
  runInRequestContext,
@@ -108,12 +96,4 @@ export type {
108
96
  StateCookieSeed,
109
97
  } from "./internal/context.js";
110
98
 
111
- // The low-level context runner that enters a RequestContext (the same one the
112
- // RSC handler uses for server actions). Re-exported so a ctx built with
113
- // createTestRequestContext can be entered directly; runInRequestContext is the
114
- // one-call convenience over createTestRequestContext + runWithRequestContext.
115
99
  export { runWithRequestContext } from "../server/request-context.js";
116
-
117
- // The E2E harness is NOT re-exported here: it must be imported from
118
- // `@rangojs/router/testing/e2e` so it stays loadable in a plain Playwright
119
- // runner (this barrel pulls in router-manifest code that needs Vite virtuals).