@rangojs/router 0.22.1 → 0.24.0

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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
package/src/ssr/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import React from "react";
2
3
  import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
3
4
  import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
@@ -263,23 +264,40 @@ export interface SSRDependencies<TEnv = unknown> {
263
264
  */
264
265
  const activeCaptureClientLoads = new Set<Set<PromiseLike<unknown>>>();
265
266
 
266
- /** The loader wrapper {@link captureClientLoads} last installed. */
267
+ /**
268
+ * Dev only: the client-reference loads requested by the Flight decode of the
269
+ * document render whose context this is (see settleDocumentClientLoads).
270
+ *
271
+ * Gated on `process.env.NODE_ENV`, the router's dev signal, not on the
272
+ * `import.meta.env.DEV` that adds plugin-rsc's cache tag: a build folds it
273
+ * (the app's NODE_ENV define, or the Cloudflare plugin), so the hold is
274
+ * compiled out. A server where the two disagree loses nothing: a dev server
275
+ * run with NODE_ENV=production streams as it did before the hold, and an
276
+ * unfolded production server collects only loads still pending, the first
277
+ * load of each module, bounded like any other.
278
+ */
279
+ const documentClientLoads =
280
+ process.env.NODE_ENV !== "production"
281
+ ? new AsyncLocalStorage<Set<PromiseLike<unknown>>>()
282
+ : undefined;
283
+
284
+ /** Bound on settleDocumentClientLoads; a later load flushes as before. */
285
+ const DEV_DOCUMENT_CLIENT_LOAD_WAIT_MS = 5_000;
286
+
287
+ /** The loader wrapper {@link installClientLoadRecorder} last installed. */
267
288
  let recordingClientReferenceLoader: ((id: string) => unknown) | undefined;
268
289
 
269
290
  /**
270
- * Start collecting the client-reference loads requested during a capture;
271
- * call the returned `stop` when the capture ends. Wraps plugin-rsc's SSR
272
- * loader (`globalThis.__vite_rsc_client_require__`, which
273
- * `__vite_rsc_require__` reads per call) on the first capture, so an isolate
274
- * that never captures keeps the bare loader. Wraps again when another wrapper
275
- * replaced this one (the build shell phase bridges hashed ids the same way,
291
+ * Wrap plugin-rsc's SSR loader (`globalThis.__vite_rsc_client_require__`,
292
+ * which `__vite_rsc_require__` reads per call) to collect pending loads into
293
+ * every running capture's set and the current dev document's set. Installed
294
+ * on the first capture or dev document, so a production isolate that never
295
+ * captures keeps the bare loader. Wraps again when another wrapper replaced
296
+ * this one (the build shell phase bridges hashed ids the same way,
276
297
  * vite/discovery/shell-prerender-phase.ts). Collects nothing without the
277
298
  * loader.
278
299
  */
279
- function captureClientLoads(): {
280
- loads: Set<PromiseLike<unknown>>;
281
- stop: () => void;
282
- } {
300
+ function installClientLoadRecorder(): void {
283
301
  const g = globalThis as {
284
302
  __vite_rsc_client_require__?: (id: string) => unknown;
285
303
  };
@@ -287,24 +305,104 @@ function captureClientLoads(): {
287
305
  if (typeof load === "function" && load !== recordingClientReferenceLoader) {
288
306
  recordingClientReferenceLoader = (id) => {
289
307
  const loading = load(id);
308
+ const documentLoads = documentClientLoads?.getStore();
290
309
  // plugin-rsc memoizes one promise per id and the Flight client stamps
291
310
  // status "fulfilled" on it once loaded: a warm module is not collected.
292
311
  if (
293
- activeCaptureClientLoads.size > 0 &&
312
+ (activeCaptureClientLoads.size > 0 || documentLoads) &&
294
313
  isThenable(loading) &&
295
314
  (loading as { status?: string }).status !== "fulfilled"
296
315
  ) {
297
316
  for (const loads of activeCaptureClientLoads) loads.add(loading);
317
+ if (documentLoads) {
318
+ // The document's set holds only loads still pending.
319
+ documentLoads.add(loading);
320
+ const drop = (): void => {
321
+ documentLoads.delete(loading);
322
+ };
323
+ loading.then(drop, drop);
324
+ }
298
325
  }
299
326
  return loading;
300
327
  };
301
328
  g.__vite_rsc_client_require__ = recordingClientReferenceLoader;
302
329
  }
330
+ }
331
+
332
+ /**
333
+ * Start collecting the client-reference loads requested during a capture;
334
+ * call the returned `stop` when the capture ends.
335
+ */
336
+ function captureClientLoads(): {
337
+ loads: Set<PromiseLike<unknown>>;
338
+ stop: () => void;
339
+ } {
340
+ installClientLoadRecorder();
303
341
  const loads = new Set<PromiseLike<unknown>>();
304
342
  activeCaptureClientLoads.add(loads);
305
343
  return { loads, stop: () => activeCaptureClientLoads.delete(loads) };
306
344
  }
307
345
 
346
+ /**
347
+ * Dev only: when the shell is ready, hold the first flush until the
348
+ * client-reference loads still pending at that moment have settled, so a
349
+ * boundary waiting on one completes in place, as in a build (#1106).
350
+ *
351
+ * plugin-rsc tags client reference ids with a per-render cache tag in dev
352
+ * (createClientManifest in @vitejs/plugin-rsc core/rsc.js), so its SSR loader
353
+ * memo misses on every request and each reference loads through the module
354
+ * runner again: an RPC per import on workerd. A client component a loader's
355
+ * answer draws under an inline <Suspense> resolved after the shell flushed,
356
+ * and the document streamed the fallback, which a browser without JavaScript
357
+ * never replaces. A build loads each reference once per isolate and reads it
358
+ * synchronously after that.
359
+ *
360
+ * Only that snapshot is waited for: a load starts when the row naming it
361
+ * arrives, so its data is already in the payload. A load requested later
362
+ * belongs to data that arrives after the shell, which streams as in a build,
363
+ * and nothing pending means no wait at all: the router adds no wait of its
364
+ * own, so a loading() skeleton or an inline fallback still streams.
365
+ */
366
+ async function settleDocumentClientLoads(
367
+ pending: Set<PromiseLike<unknown>>,
368
+ ): Promise<void> {
369
+ const snapshot = [...pending];
370
+ if (snapshot.length === 0) return;
371
+ const deadline = createCancelableTimeout(DEV_DOCUMENT_CLIENT_LOAD_WAIT_MS);
372
+ try {
373
+ const expired = await Promise.race([
374
+ Promise.allSettled(snapshot).then(() => false),
375
+ deadline.promise.then(() => true),
376
+ ]);
377
+ if (expired) return;
378
+ // A settled load pings the Fizz task that waited on it, which retries on
379
+ // a setTimeout(0) turn (pingTask).
380
+ await macrotask();
381
+ await macrotask();
382
+ } finally {
383
+ deadline.cancel();
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Run a document's Flight decode. In dev it runs in its own client-load set,
389
+ * and `settle` holds the first flush until the loads pending when the shell
390
+ * is ready have settled (settleDocumentClientLoads); a build gets the decode
391
+ * alone.
392
+ */
393
+ function trackDocumentClientLoads<T>(decode: () => T): {
394
+ value: T;
395
+ settle?: () => Promise<void>;
396
+ } {
397
+ if (!documentClientLoads) return { value: decode() };
398
+ installClientLoadRecorder();
399
+ const loads = new Set<PromiseLike<unknown>>();
400
+ return {
401
+ value: documentClientLoads.run(loads, decode),
402
+ settle: () => settleDocumentClientLoads(loads),
403
+ };
404
+ }
405
+
308
406
  /**
309
407
  * Route an SSR error through the deps.onError notification callback with the
310
408
  * "rendering" phase. Swallows callback failures so a broken reporter never
@@ -708,10 +806,10 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
708
806
  // The parallel no-op catch keeps a payload rejection off the
709
807
  // unhandledRejection path if fizz errors before consuming the promise;
710
808
  // SsrRoot's React.use() on the SAME promise still observes the rejection.
711
- const payload = deserializeSsrPayload(
712
- createFromReadableStream,
713
- rscStream1,
714
- );
809
+ const { value: payload, settle: settleClientLoads } =
810
+ trackDocumentClientLoads(() =>
811
+ deserializeSsrPayload(createFromReadableStream, rscStream1),
812
+ );
715
813
  payload.catch(() => {});
716
814
 
717
815
  const SsrRoot = createSsrRootComponent({
@@ -763,6 +861,10 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
763
861
  }),
764
862
  );
765
863
 
864
+ // Dev only: the client-reference loads the decode requested settle
865
+ // before the first flush (trackDocumentClientLoads).
866
+ if (settleClientLoads) await settleClientLoads();
867
+
766
868
  // Wait for all Suspense boundaries to resolve when streamMode is "allReady".
767
869
  // This buffers the entire HTML before flushing — used for bots that
768
870
  // cannot process streamed HTML.
@@ -12,8 +12,7 @@
12
12
  * Post the result with `serveShellRequest(router, url, { form })`.
13
13
  */
14
14
 
15
- import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
16
- import { registerServerModule } from "./internal/server-modules.js";
15
+ import { registerTestAction } from "./internal/test-action.js";
17
16
 
18
17
  /** Options for {@link createActionForm}. */
19
18
  export interface CreateActionFormOptions {
@@ -41,15 +40,7 @@ export function createActionForm(
41
40
  options: CreateActionFormOptions,
42
41
  ): FormData {
43
42
  const { id, useActionState = true, fields = {} } = options;
44
- const [moduleId, name] = id.split("#");
45
- if (!moduleId || !name) {
46
- throw new Error(
47
- `createActionForm: id must be "<module id>#<export name>", received ${JSON.stringify(id)}`,
48
- );
49
- }
50
- registerServerModule(moduleId, {
51
- [name]: RSDServer.registerServerReference(action, moduleId, name),
52
- });
43
+ registerTestAction(action, id, "createActionForm");
53
44
 
54
45
  const form = new FormData();
55
46
  if (useActionState) {
@@ -106,16 +106,15 @@ import {
106
106
  runWithRequestContext,
107
107
  setRequestContextParams,
108
108
  } from "../server/request-context.js";
109
+ import { REWRITE_HEADER, runRewritePasses } from "../rsc/rewrite-passes.js";
110
+ import { defineRewrittenFrom } from "../server/rewrite-trail.js";
109
111
  import { resolveRouterVersions } from "../server/build-version-table.js";
110
112
  import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
111
113
  import type {
112
114
  MiddlewareEntry,
113
115
  MiddlewareFn,
114
116
  } from "../router/middleware-types.js";
115
- import {
116
- createReverseFunction,
117
- stripInternalParams,
118
- } from "../router/handler-context.js";
117
+ import { createReverseFunction } from "../router/handler-context.js";
119
118
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
120
119
  import {
121
120
  compileSearchParamsFilter,
@@ -135,6 +134,7 @@ import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
135
134
  import { RouterError } from "../errors.js";
136
135
  import { createProblemDetails } from "../rsc/response-error.js";
137
136
  import {
137
+ createReloadResponse,
138
138
  createResponseWithMergedHeaders,
139
139
  createSimpleRedirectResponse,
140
140
  finalizeResponse,
@@ -143,7 +143,7 @@ import {
143
143
  } from "../rsc/helpers.js";
144
144
  import { guardOutgoingRedirect } from "../rsc/redirect-guard.js";
145
145
  import { stringifyJsonRouteResult } from "../rsc/json-route-result.js";
146
- import { isWebSocketUpgradeResponse } from "../response-utils.js";
146
+ import { isWebSocketUpgradeResponse, trySetHeader } from "../response-utils.js";
147
147
  import { invokeOnError } from "../router/error-handling.js";
148
148
  import type { OnErrorCallback } from "../types/error-types.js";
149
149
  import type { Rango } from "../router/router-interfaces.js";
@@ -321,6 +321,68 @@ function serializeResponseRouteError(
321
321
  });
322
322
  }
323
323
 
324
+ /** What one pass of a dispatch matched (the visitor's URL, or a rewrite target). */
325
+ interface DispatchPass<TEnv> {
326
+ req: Request;
327
+ url: URL;
328
+ redirectTo?: string;
329
+ isUnmatched: boolean;
330
+ preview: Awaited<ReturnType<DispatchableRouter<TEnv>["previewMatch"]>>;
331
+ responseType?: string;
332
+ handler?: Function;
333
+ params: Record<string, string>;
334
+ routeKey?: string;
335
+ }
336
+
337
+ async function matchDispatchPass<TEnv>(
338
+ router: DispatchableRouter<TEnv>,
339
+ req: Request,
340
+ env: TEnv,
341
+ ): Promise<DispatchPass<TEnv>> {
342
+ const url = new URL(req.url);
343
+ // findMatch carries trailing-slash/redirect targets and null on no match.
344
+ // previewMatch swallows redirects, so detect them here first.
345
+ const match = await router.findMatch(url.pathname);
346
+ const redirectTo = match?.redirectTo;
347
+ const isUnmatched = !match;
348
+
349
+ // previewMatch resolves responseType, the response-route handler, and the
350
+ // route middleware from the matched entry tree (with content negotiation).
351
+ // Skip it for a redirect/unmatched path — there is no response route to
352
+ // resolve, and previewMatch would return null / a redirect marker anyway.
353
+ const preview =
354
+ redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
355
+
356
+ // A bare match with no responseType is an RSC route. The RSC-route throw is a
357
+ // hard boundary of this primitive (no Flight runtime), distinct from the
358
+ // 308/404 outcomes, so it stays a pre-middleware guard.
359
+ const responseType = preview?.responseType;
360
+ const handler = preview?.handler;
361
+ if (
362
+ !redirectTo &&
363
+ !isUnmatched &&
364
+ (!responseType || typeof handler !== "function")
365
+ ) {
366
+ throw new Error(
367
+ `dispatch() does not render RSC routes — the route matched at ` +
368
+ `"${url.pathname}" is a React Server Component route, not a response ` +
369
+ `route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
370
+ `e2e test to exercise component rendering.`,
371
+ );
372
+ }
373
+ return {
374
+ req,
375
+ url,
376
+ redirectTo,
377
+ isUnmatched,
378
+ preview,
379
+ responseType,
380
+ handler,
381
+ params: preview?.params ?? match?.params ?? {},
382
+ routeKey: preview?.routeKey ?? match?.routeKey,
383
+ };
384
+ }
385
+
324
386
  /**
325
387
  * Run a request through the router in-process and return the Response.
326
388
  *
@@ -353,39 +415,10 @@ export async function dispatch<TEnv = any>(
353
415
  setRouterManifest(routerId, router.routeMap as Record<string, string>);
354
416
  }
355
417
 
356
- // findMatch carries trailing-slash/redirect targets and null on no match.
357
- // previewMatch swallows redirects, so detect them here first.
358
- const match = await router.findMatch(url.pathname);
359
- const redirectTo = match?.redirectTo;
360
- const isUnmatched = !match;
361
-
362
- // previewMatch resolves responseType, the response-route handler, and the
363
- // route middleware from the matched entry tree (with content negotiation).
364
- // Skip it for a redirect/unmatched path — there is no response route to
365
- // resolve, and previewMatch would return null / a redirect marker anyway.
366
- const preview =
367
- redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
368
-
369
- // A bare match with no responseType is an RSC route. The RSC-route throw is a
370
- // hard boundary of this primitive (no Flight runtime), distinct from the
371
- // 308/404 outcomes below, so it stays a pre-middleware guard.
372
- const responseType = preview?.responseType;
373
- const handler = preview?.handler;
374
- const params = preview?.params ?? match?.params ?? {};
375
- const routeKey = preview?.routeKey ?? match?.routeKey;
376
-
377
- if (
378
- !redirectTo &&
379
- !isUnmatched &&
380
- (!responseType || typeof handler !== "function")
381
- ) {
382
- throw new Error(
383
- `dispatch() does not render RSC routes — the route matched at ` +
384
- `"${url.pathname}" is a React Server Component route, not a response ` +
385
- `route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
386
- `e2e test to exercise component rendering.`,
387
- );
388
- }
418
+ // The visitor's URL is matched before any middleware runs, so an RSC route
419
+ // fails here, before a middleware side effect. A rewrite target is matched
420
+ // the same way, at the start of its pass (below).
421
+ const firstPass = await matchDispatchPass(router, req, env);
389
422
 
390
423
  const variables: Record<string, unknown> = {};
391
424
 
@@ -474,29 +507,28 @@ export async function dispatch<TEnv = any>(
474
507
  // Set params before middleware/handler run, so global middleware sees
475
508
  // ctx.params (production sets them during matching, before middleware).
476
509
  // On a redirect/unmatched path there are no route params.
477
- if (routeKey !== undefined) {
478
- setRequestContextParams(params, routeKey);
479
- } else {
480
- requestContext.params = params;
481
- }
510
+ const recordPassParams = (pass: DispatchPass<TEnv>): void => {
511
+ if (pass.routeKey !== undefined) {
512
+ setRequestContextParams(pass.params, pass.routeKey);
513
+ } else {
514
+ requestContext.params = pass.params;
515
+ }
516
+ };
482
517
 
483
518
  // The response-route handler (with its own route middleware) lives inside
484
519
  // coreHandler below, mirroring production where handleResponseRoute is
485
520
  // nested inside coreHandler. Built lazily so a redirect/404 path never
486
521
  // touches it.
487
- const callResponseRoute = async (): Promise<Response> => {
522
+ const callResponseRoute = async (
523
+ pass: DispatchPass<TEnv>,
524
+ ): Promise<Response> => {
525
+ const { req, url, preview, params, responseType, handler } = pass;
488
526
  // Match production: a partial (client-navigation) request to a response
489
527
  // route is short-circuited to X-RSC-Reload (handleResponseRoute), BEFORE
490
528
  // route-level middleware runs. Route-level middleware is skipped on a
491
529
  // partial, exactly as production skips it.
492
530
  const partialFinalHandler = async (): Promise<Response> =>
493
- createResponseWithMergedHeaders(null, {
494
- status: 200,
495
- headers: {
496
- "X-RSC-Reload": stripInternalParams(url).toString(),
497
- "content-type": "text/x-component;charset=utf-8",
498
- },
499
- });
531
+ createReloadResponse(requestContext, url);
500
532
 
501
533
  const cleanUrl = new URL(req.url);
502
534
  for (const key of [...cleanUrl.searchParams.keys()]) {
@@ -523,6 +555,7 @@ export async function dispatch<TEnv = any>(
523
555
  // Brand as request-scoped so a "use cache" inside a response-route handler
524
556
  // is detected as a request-scope violation here exactly as in production
525
557
  // (response-route-handler.ts brands the same shape).
558
+ defineRewrittenFrom(responseHandlerCtx, requestContext);
526
559
  (responseHandlerCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
527
560
 
528
561
  const callHandler = async (): Promise<Response> => {
@@ -668,8 +701,11 @@ export async function dispatch<TEnv = any>(
668
701
  // onTimeout Response is RETURNED from coreHandler so it flows back out
669
702
  // through the global chain and finalizeResponse, identical to production
670
703
  // (handler.ts:857 returns handleTimeoutResponse into the same next() path).
671
- const runResponseRoute = async (): Promise<Response> => {
672
- const responseWork = callResponseRoute();
704
+ const runResponseRoute = async (
705
+ pass: DispatchPass<TEnv>,
706
+ ): Promise<Response> => {
707
+ const { routeKey } = pass;
708
+ const responseWork = callResponseRoute(pass);
673
709
  const renderOutcome = await withTimeout(
674
710
  responseWork,
675
711
  router.timeouts?.renderStartMs,
@@ -752,7 +788,8 @@ export async function dispatch<TEnv = any>(
752
788
  // wins, never reaching the 308/404. The renderStartMs deadline lives inside
753
789
  // runResponseRoute (below the chain), never covering the 308/404 paths —
754
790
  // production only wraps the response-route plan (handler.ts:834-856).
755
- const coreHandler = async (): Promise<Response> => {
791
+ const coreHandler = async (pass: DispatchPass<TEnv>): Promise<Response> => {
792
+ const { redirectTo, isUnmatched, url } = pass;
756
793
  if (redirectTo) {
757
794
  return createResponseWithMergedHeaders(null, {
758
795
  status: 308,
@@ -765,7 +802,7 @@ export async function dispatch<TEnv = any>(
765
802
  headers: { "content-type": "text/plain;charset=utf-8" },
766
803
  });
767
804
  }
768
- return runResponseRoute();
805
+ return runResponseRoute(pass);
769
806
  };
770
807
 
771
808
  // request.start opens the match transaction, mirroring match-handlers.ts.
@@ -790,18 +827,49 @@ export async function dispatch<TEnv = any>(
790
827
  // only the response-route unit inside coreHandler (see runResponseRoute),
791
828
  // mirroring handler.ts:845 so global middleware time is never counted
792
829
  // toward the deadline.
793
- const globalMatches = matchMiddleware(url.pathname, router.middleware);
794
- const mwResponse =
795
- globalMatches.length === 0
796
- ? await coreHandler()
797
- : await executeMiddleware<TEnv>(
798
- globalMatches,
799
- req,
800
- env,
801
- variables,
802
- coreHandler,
803
- reverse,
804
- );
830
+ // A throw rewrite() re-dispatches the same request context as the
831
+ // target, through the production handler's pass loop
832
+ // (rsc/rewrite-passes.ts). Each pass matches its URL and builds its own
833
+ // core handler.
834
+ let passIndex = 0;
835
+ const { response: mwResponse, rewrittenTo } = await runRewritePasses({
836
+ reqCtx: requestContext,
837
+ request: req,
838
+ url,
839
+ routeMap: () => router.routeMap as Record<string, string>,
840
+ basename: router.basename,
841
+ runPass: async (passRequest) => {
842
+ const pass =
843
+ passIndex++ === 0
844
+ ? firstPass
845
+ : await matchDispatchPass(router, passRequest, env);
846
+ recordPassParams(pass);
847
+ const globalMatches = matchMiddleware(
848
+ pass.url.pathname,
849
+ router.middleware,
850
+ );
851
+ const passCore = (): Promise<Response> => coreHandler(pass);
852
+ return globalMatches.length === 0
853
+ ? passCore()
854
+ : executeMiddleware<TEnv>(
855
+ globalMatches,
856
+ pass.req,
857
+ env,
858
+ variables,
859
+ passCore,
860
+ reverse,
861
+ );
862
+ },
863
+ report: (error, at) => {
864
+ invokeOnError(
865
+ router.onError,
866
+ error,
867
+ "routing",
868
+ { request: at.request, url: at.url, env, handledByBoundary: false },
869
+ "RSC",
870
+ );
871
+ },
872
+ });
805
873
 
806
874
  // Match production's global-chain exit (handler.ts): on a partial/action
807
875
  // request a middleware 3xx redirect is converted to a Flight-safe response
@@ -832,6 +900,9 @@ export async function dispatch<TEnv = any>(
832
900
  } else {
833
901
  finalResponse = finalizeResponse(mwResponse);
834
902
  }
903
+ if (rewrittenTo !== undefined) {
904
+ trySetHeader(finalResponse, REWRITE_HEADER, rewrittenTo);
905
+ }
835
906
 
836
907
  // request.end closes the transaction. dispatch produces no RSC segments and
837
908
  // holds no match-cache state, so segmentCount/cacheHit are 0/false — the
@@ -3,6 +3,7 @@
3
3
  // returned from `createPageHelpers(expect)`; the rest are standalone.
4
4
 
5
5
  import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
6
+ import { PREFETCH_HEADER } from "../../prefetch-markers.js";
6
7
 
7
8
  // ============================================================================
8
9
  // Standalone helpers (need only a page/locator)
@@ -170,7 +171,8 @@ export async function measureTime<T>(
170
171
  export function isPrefetchRequest(req: {
171
172
  headers: () => Record<string, string>;
172
173
  }): boolean {
173
- return !!req.headers()["x-rango-prefetch"];
174
+ // Playwright lowercases header names.
175
+ return !!req.headers()[PREFETCH_HEADER.toLowerCase()];
174
176
  }
175
177
 
176
178
  // Module-level matcher + handler so blockPrefetch/unblockPrefetch share
@@ -64,6 +64,7 @@ export {
64
64
  resetShellTestState,
65
65
  } from "./serve-shell-request.js";
66
66
  export type {
67
+ ServeShellRequestAction,
67
68
  ServeShellRequestOptions,
68
69
  ServeShellRequestResult,
69
70
  ShellRequestHandles,
@@ -26,7 +26,7 @@
26
26
  * condition and would throw if pulled into this barrel.
27
27
  *
28
28
  * Layers:
29
- * - Unit: runMiddleware, runLoader, runClientRevalidate
29
+ * - Unit: runMiddleware, runLoader, runRevalidate, runTransitionWhen
30
30
  * - Integration: dispatch (request -> Response)
31
31
  * - Cross-cut: assertCacheStatus, assertShellStatus, assertGeneratedRoutesMatch
32
32
  * - Component: see @rangojs/router/testing/dom (renderRoute)
@@ -38,6 +38,7 @@ export { runMiddleware } from "./run-middleware.js";
38
38
  export type {
39
39
  RunMiddlewareOptions,
40
40
  RunMiddlewareResult,
41
+ RunMiddlewareRewrite,
41
42
  } from "./run-middleware.js";
42
43
  export { runLoader, runLoaderResult } from "./run-loader.js";
43
44
  export type {
@@ -55,8 +56,11 @@ export type {
55
56
  RunTransitionWhenResult,
56
57
  } from "./run-transition-when.js";
57
58
 
58
- export { runClientRevalidate } from "./run-client-revalidate.js";
59
- export type { RunClientRevalidateOptions } from "./run-client-revalidate.js";
59
+ export { runRevalidate } from "./run-revalidate.js";
60
+ export type {
61
+ RunRevalidateAction,
62
+ RunRevalidateOptions,
63
+ } from "./run-revalidate.js";
60
64
 
61
65
  export { dispatch } from "./dispatch.js";
62
66
  export type { DispatchOptions } from "./dispatch.js";
@@ -1,5 +1,20 @@
1
1
  import { resolveActionRefId } from "../../router/is-action.js";
2
2
 
3
+ /** An imported server action, as a testing primitive's `action` option takes it. */
4
+ export type ImportedAction = (...args: never[]) => unknown;
5
+
6
+ /**
7
+ * Normalizes a testing primitive's `action` option: a bare imported action or
8
+ * actionId string becomes `{ ref }`; the object form passes through.
9
+ */
10
+ export function normalizeTestAction<
11
+ T extends { ref?: ImportedAction | string },
12
+ >(action: ImportedAction | string | T | undefined): T | undefined {
13
+ return typeof action === "function" || typeof action === "string"
14
+ ? ({ ref: action } as T)
15
+ : action;
16
+ }
17
+
3
18
  /**
4
19
  * The id a testing primitive's `action` option names: a single imported
5
20
  * server action (its `$id ?? $$id`, as the action boundary derives it) or a
@@ -7,7 +22,7 @@ import { resolveActionRefId } from "../../router/is-action.js";
7
22
  * primitive.
8
23
  */
9
24
  export function resolveTestActionId(
10
- action: ((...args: never[]) => unknown) | string | undefined,
25
+ action: ImportedAction | string | undefined,
11
26
  primitive: string,
12
27
  ): string | undefined {
13
28
  if (action === undefined) return undefined;