@rangojs/router 0.21.0 → 0.22.1

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 (244) hide show
  1. package/dist/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
@@ -0,0 +1,66 @@
1
+ /**
2
+ * createActionForm: the FormData a browser posts when a form is submitted
3
+ * before hydration (no JavaScript), for a server action.
4
+ *
5
+ * React's server render writes hidden fields on the form so the POST names its
6
+ * action and carries the bound state; decodeAction / decodeFormState read them
7
+ * back. A test cannot render those fields (react-dom/server does not load under
8
+ * the react-server condition), so this builds the same wire format and
9
+ * registers the action where the plugin-rsc stub's decodeAction resolves it,
10
+ * tagged as a server reference like a built "use server" export so the bound
11
+ * action keeps its `$$id` (onError's actionId, the action phase label).
12
+ * Post the result with `serveShellRequest(router, url, { form })`.
13
+ */
14
+
15
+ import * as RSDServer from "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge";
16
+ import { registerServerModule } from "./internal/server-modules.js";
17
+
18
+ /** Options for {@link createActionForm}. */
19
+ export interface CreateActionFormOptions {
20
+ /**
21
+ * The action's reference id, `<module id>#<export name>`, as the build gives
22
+ * a "use server" export. Any unique string of that shape in a test.
23
+ */
24
+ id: string;
25
+ /**
26
+ * A `useActionState` form (default): the action runs as `(prevState, formData)`
27
+ * and the POST carries the state key. false: a plain `<form action={fn}>`
28
+ * ($ACTION_ID_), the action runs as `(formData)`.
29
+ */
30
+ useActionState?: boolean;
31
+ /** The form's own fields, as the person filled them. */
32
+ fields?: Record<string, string>;
33
+ }
34
+
35
+ /**
36
+ * Build a no-JS form POST body for `action` and register it so a handler
37
+ * decodes it back to `action`.
38
+ */
39
+ export function createActionForm(
40
+ action: (...args: any[]) => unknown,
41
+ options: CreateActionFormOptions,
42
+ ): FormData {
43
+ 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
+ });
53
+
54
+ const form = new FormData();
55
+ if (useActionState) {
56
+ form.set("$ACTION_REF_1", "");
57
+ form.set("$ACTION_1:0", JSON.stringify({ id, bound: "$@1" }));
58
+ // The state the hook held when the form was submitted: its initial null.
59
+ form.set("$ACTION_1:1", "[null]");
60
+ form.set("$ACTION_KEY", "k0");
61
+ } else {
62
+ form.set(`$ACTION_ID_${id}`, "");
63
+ }
64
+ for (const [field, value] of Object.entries(fields)) form.set(field, value);
65
+ return form;
66
+ }
@@ -22,6 +22,33 @@ declare module "@vitejs/plugin-rsc/vendor/react-server-dom/server.edge" {
22
22
  /** Server-side temporary-reference set (a WeakMap). */
23
23
  export function createTemporaryReferenceSet(): unknown;
24
24
 
25
+ /** Resolve a no-JS form POST to its action, bound to the form data; null for a plain form. */
26
+ export function decodeAction(
27
+ body: FormData,
28
+ serverManifest: unknown,
29
+ ): Promise<unknown> | null;
30
+
31
+ /** Decode an action's encoded arguments; rejects for a raw form body. */
32
+ export function decodeReply(
33
+ body: string | FormData,
34
+ serverManifest: unknown,
35
+ options?: object,
36
+ ): Promise<unknown>;
37
+
38
+ /** The useActionState form state of a no-JS POST, or null. */
39
+ export function decodeFormState(
40
+ actionResult: unknown,
41
+ body: FormData,
42
+ serverManifest: unknown,
43
+ ): Promise<unknown>;
44
+
45
+ /** Tag a function as a server reference; `$$id` becomes `${id}#${exportName}`. */
46
+ export function registerServerReference<T>(
47
+ reference: T,
48
+ id: string,
49
+ exportName: string,
50
+ ): T;
51
+
25
52
  /**
26
53
  * Tag a value as a client reference. Mutates `impl` in place (defining
27
54
  * `$$typeof`/`$$id`/`$$async`) and returns it, so a server tree that imports
@@ -56,6 +56,9 @@ export type {
56
56
  StateCookieSeed,
57
57
  } from "./render-handler.js";
58
58
 
59
+ export { createActionForm } from "./action-form.js";
60
+ export type { CreateActionFormOptions } from "./action-form.js";
61
+
59
62
  export {
60
63
  serveShellRequest,
61
64
  resetShellTestState,
@@ -61,6 +61,25 @@ export type { RunClientRevalidateOptions } from "./run-client-revalidate.js";
61
61
  export { dispatch } from "./dispatch.js";
62
62
  export type { DispatchOptions } from "./dispatch.js";
63
63
 
64
+ // On-demand prerender: an in-memory WritablePrerenderStore fake for unit-testing
65
+ // code that calls router.prerender(). Dep-free (a plain Map), so it stays on this
66
+ // React/RSC-free barrel. See @rangojs/router/prerender for the store contracts.
67
+ export { createMemoryPrerenderStore } from "../prerender/memory-prerender-store.js";
68
+ export type {
69
+ MemoryPrerenderStore,
70
+ MemoryPrerenderStoreOptions,
71
+ } from "../prerender/memory-prerender-store.js";
72
+ export type {
73
+ WritablePrerenderStore,
74
+ PrerenderStoredEntry,
75
+ PrerenderKey,
76
+ } from "../prerender/writable-store.js";
77
+ export type {
78
+ PrerenderConfig,
79
+ PrerenderResult,
80
+ PrerenderWarmCaches,
81
+ } from "../prerender/on-demand.js";
82
+
64
83
  export {
65
84
  assertCacheStatus,
66
85
  assertCacheDecision,
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Server reference modules a test registered, by the module id the action's
3
+ * `$$id` (`<module id>#<export name>`) names. The plugin-rsc stub's module
4
+ * loader reads this when decodeAction resolves a no-JS form POST to its action.
5
+ */
6
+ const SERVER_MODULES: Map<string, Record<string, unknown>> = new Map();
7
+
8
+ /**
9
+ * Register `exports` as the server module `id`, merging into an earlier call.
10
+ * The module object is kept and mutated: the stub's require is memoized per
11
+ * id, so a later export must appear on the object it first returned.
12
+ */
13
+ export function registerServerModule(
14
+ id: string,
15
+ exports: Record<string, unknown>,
16
+ ): void {
17
+ const existing = SERVER_MODULES.get(id);
18
+ if (existing) Object.assign(existing, exports);
19
+ else SERVER_MODULES.set(id, { ...exports });
20
+ }
21
+
22
+ /** The module a decoded server reference resolves to. */
23
+ export function loadServerModule(id: string): Record<string, unknown> {
24
+ const found = SERVER_MODULES.get(id);
25
+ if (!found) {
26
+ throw new Error(
27
+ `plugin-rsc stub: server reference module "${id}" is not loadable in a test. ` +
28
+ "Build the form with createActionForm() so its action is registered.",
29
+ );
30
+ }
31
+ return found;
32
+ }
@@ -110,9 +110,9 @@ import type { ThemeConfig } from "../theme/types.js";
110
110
  import { resolveThemeConfig } from "../theme/constants.js";
111
111
  import { isUnderTestRunner } from "../runtime-env.js";
112
112
  import { setupNavigationBridgeDelegatedPrefetch } from "../browser/navigation-bridge.js";
113
- import { resetAdaptiveStrategyForTesting } from "../browser/prefetch/default-strategy.js";
114
113
  import { resetPrefetchObserverForTesting } from "../browser/prefetch/observer.js";
115
114
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
115
+ import { auditTreeCause } from "../suspense-audit.js";
116
116
 
117
117
  const TEST_ORIGIN = "http://localhost";
118
118
  let activePrefetchRegistrations = 0;
@@ -491,7 +491,6 @@ function DelegatedPrefetchRegistration({
491
491
  }
492
492
  pendingPrefetchReset = undefined;
493
493
  resetPrefetchObserverForTesting();
494
- resetAdaptiveStrategyForTesting();
495
494
  });
496
495
  }
497
496
  }
@@ -529,13 +528,14 @@ function HydratedMarker(): null {
529
528
  }
530
529
 
531
530
  /**
532
- * Applies `lateHandles` from an effect after the tree's: where production's
533
- * root effect releases the document's late handle channel
534
- * (browser/rsc-router.tsx hydrationCommitted). A boundary that hydrates in a
535
- * later pass finds the update applied.
531
+ * Runs `run` from an effect after the tree's. That is where production's late
532
+ * handle channel writes a push that arrives after the root hydrated (the store
533
+ * holds it while the hydration window is open, EventController.lockHydration),
534
+ * and where the document has been revealed (browser/rsc-router.tsx
535
+ * documentRevealed).
536
536
  */
537
- function LateHandles({ apply }: { apply: () => void }): null {
538
- useEffect(apply, [apply]);
537
+ function AfterTree({ run }: { run: () => void }): null {
538
+ useEffect(run, [run]);
539
539
  return null;
540
540
  }
541
541
 
@@ -751,8 +751,15 @@ export async function renderRoute(
751
751
  const resolvedSeed = await resolveDeferredHandleValues(handleSeed);
752
752
  const initialSegmentIds = initialSegments.map((s) => s.id);
753
753
  eventController.setHandleData(resolvedSeed, initialSegmentIds);
754
- // As initBrowserApp before hydrateRoot: what a hydrating reader reads.
755
- eventController.freezeHydrationHandleState();
754
+ // As initBrowserApp before hydrateRoot: until the provider's barrier commit,
755
+ // handle reads return this state and later writes wait.
756
+ if (options.hydrate) eventController.lockHydration();
757
+ let resolveDocumentRevealed: (() => void) | undefined;
758
+ const documentRevealed = options.hydrate
759
+ ? new Promise<void>((resolve) => {
760
+ resolveDocumentRevealed = resolve;
761
+ })
762
+ : undefined;
756
763
  // A late update carries the full state: the seed with these handles' values
757
764
  // replaced, resolved as the browser resolves a late yield before applying.
758
765
  let applyLateHandles: (() => void) | undefined;
@@ -866,11 +873,15 @@ export async function renderRoute(
866
873
  }
867
874
  const emit: UpdateSubscriber = (update) => store.emitUpdate(update);
868
875
  await act(async () => {
876
+ // As production's emitters name their cause (suspense-audit.ts I6).
877
+ auditTreeCause("navigation");
869
878
  // Production's transition() lane (browser/partial-update.ts). Its
870
879
  // same-structure / fully-prefetched / optimistic hold lanes are not
871
880
  // modeled: without transition() the commit stays urgent.
872
881
  if (shouldStartViewTransition(segments, gatedOff)) {
873
- commitInTransition(emit, segments, { root, metadata }, ["navigation"]);
882
+ commitInTransition(store.loaders, emit, segments, { root, metadata }, [
883
+ "navigation",
884
+ ]);
874
885
  } else {
875
886
  emit({ root, metadata });
876
887
  }
@@ -938,11 +949,13 @@ export async function renderRoute(
938
949
  options.theme === undefined ? null : resolveThemeConfig(options.theme)
939
950
  }
940
951
  nonce={options.nonce}
952
+ hydration={documentRevealed ? { settled: documentRevealed } : undefined}
941
953
  />
942
954
  <DelegatedPrefetchRegistration bridge={bridge} />
943
955
  <LinkInterceptionRegistration bridge={bridge} />
944
956
  {options.hydrate && <HydratedMarker />}
945
- {applyLateHandles && <LateHandles apply={applyLateHandles} />}
957
+ {applyLateHandles && <AfterTree run={applyLateHandles} />}
958
+ {resolveDocumentRevealed && <AfterTree run={resolveDocumentRevealed} />}
946
959
  </>
947
960
  );
948
961
 
@@ -27,6 +27,14 @@
27
27
  * capture refuses for a root postpone (a live loader read with no boundary
28
28
  * above it) or an SSR render error is captured here.
29
29
  *
30
+ * A `Prerender` route serves its handler layer from the artifact
31
+ * `router.matchForPrerender` bakes for the URL (on its first request, kept
32
+ * until resetShellTestState), through the production prerender store. Its
33
+ * first `ppr` request is a MISS with a runtime capture, as a URL without a
34
+ * build-time shell is in production; build-time shells are e2e-only. An
35
+ * on-demand route (`Prerender(..., { onDemand })`) bakes nothing here: it
36
+ * serves from its prerender store, as a refresh-only page does in production.
37
+ *
30
38
  * Must run under the `react-server` condition (the rsc Vitest project), with
31
39
  * `rangoTestAliases()` resolving `@vitejs/plugin-rsc/rsc/server` to the stub
32
40
  * that carries the Flight runtime.
@@ -36,9 +44,14 @@ import { AsyncLocalStorage } from "node:async_hooks";
36
44
  import { toInternal, type Rango } from "../router/router-interfaces.js";
37
45
  import { ensureRouterManifest } from "../route-map-builder.js";
38
46
  import type { SegmentCacheStore, ShellCacheEntry } from "../cache/types.js";
47
+ import {
48
+ awaitLaterMillisecond,
49
+ createCollectingExecutionContext,
50
+ } from "../cache/background-task.js";
39
51
  import type { ExecutionContext } from "../types/request-scope.js";
40
52
  import type { HandlerCacheConfig, SSRModule } from "../rsc/types.js";
41
53
  import type { createRSCHandler } from "../rsc/handler.js";
54
+ import { setDefaultSSRModuleLoaderForTests } from "../rsc/ssr-module-loader.js";
42
55
  import {
43
56
  POST_QUIESCE_TASK_HOPS,
44
57
  SHELL_CAPTURE_MAX_WAIT_MS,
@@ -77,6 +90,14 @@ export interface ServeShellRequestOptions<TEnv = any> {
77
90
  * `result.replayStatus`.
78
91
  */
79
92
  partial?: true | { from?: string; segments?: readonly string[] };
93
+ /**
94
+ * POST this form as a submit before hydration: the no-JS request the
95
+ * progressive-enhancement path runs the action for and re-renders the page
96
+ * after. Build it with `createActionForm`. The response is the document
97
+ * (`flight` is its payload, `formState` what the SSR render received).
98
+ * Not with `partial`.
99
+ */
100
+ form?: FormData;
80
101
  }
81
102
 
82
103
  /** A response's handle data, as the browser consumes it. */
@@ -136,6 +157,19 @@ export interface ServeShellRequestResult {
136
157
  * values resolved. Undefined when no Flight was rendered.
137
158
  */
138
159
  readHandles(): Promise<ShellRequestHandles | undefined>;
160
+ /**
161
+ * The form state the SSR render received (the `formState` render option),
162
+ * or undefined when it received none.
163
+ */
164
+ formState: unknown;
165
+ /**
166
+ * The payload's `formState` as the browser reads it, the value the entry
167
+ * hands `hydrateRoot`. Undefined when the payload carries none (or no
168
+ * Flight was rendered). Rejects when the action's state is not
169
+ * Flight-serializable: the document still renders, and the entry
170
+ * hydrates without it.
171
+ */
172
+ readPayloadFormState(): Promise<unknown>;
139
173
  }
140
174
 
141
175
  type ShellHandler = ReturnType<typeof createRSCHandler>;
@@ -149,6 +183,8 @@ interface Recorder {
149
183
  rendered?: boolean;
150
184
  /** The HIT tail's Flight text. */
151
185
  tail?: string;
186
+ /** The formState render option of the document render. */
187
+ formState?: unknown;
152
188
  }
153
189
 
154
190
  const recorders = new AsyncLocalStorage<Recorder>();
@@ -168,9 +204,12 @@ function recordShellKey(recorder: Recorder | undefined): void {
168
204
  }
169
205
 
170
206
  const SSR_STUB: SSRModule = {
171
- async renderHTML(rscStream) {
207
+ async renderHTML(rscStream, options) {
172
208
  const recorder = recorders.getStore();
173
- if (recorder) recorder.rendered = true;
209
+ if (recorder) {
210
+ recorder.rendered = true;
211
+ recorder.formState = options?.formState ?? undefined;
212
+ }
174
213
  recordShellKey(recorder);
175
214
  return rscStream;
176
215
  },
@@ -226,8 +265,24 @@ const SSR_STUB: SSRModule = {
226
265
  },
227
266
  };
228
267
 
268
+ /**
269
+ * `router.prerender()` warms a route through `router.fetch`, whose handler is
270
+ * built with no `loadSSRModule` and so reaches for the Vite RSC module loader
271
+ * this project cannot run. It gets the stub the handlers below get:
272
+ * everything else on a warm's path (router.fetch, createRSCHandler, the match
273
+ * pipeline, the capture) is production code. A warm runs outside a
274
+ * serveShellRequest call, so the stub records nothing for it.
275
+ */
276
+ setDefaultSSRModuleLoaderForTests(async () => SSR_STUB);
277
+
229
278
  type HandleChannel = AsyncIterable<HandleData> | undefined;
230
279
 
280
+ /** Decode a Flight payload as the browser does. */
281
+ async function decodePayload<T>(flight: string): Promise<T> {
282
+ const { deserializeResult } = await import("../cache/segment-codec.js");
283
+ return deserializeResult<T>(flight);
284
+ }
285
+
231
286
  /**
232
287
  * Read a payload's two handle channels as browser/rsc-router.tsx does:
233
288
  * `handles` to its end, then every `handlesLate` state.
@@ -235,8 +290,7 @@ type HandleChannel = AsyncIterable<HandleData> | undefined;
235
290
  async function decodePayloadHandles(
236
291
  flight: string,
237
292
  ): Promise<ShellRequestHandles> {
238
- const { deserializeResult } = await import("../cache/segment-codec.js");
239
- const { metadata } = await deserializeResult<{
293
+ const { metadata } = await decodePayload<{
240
294
  metadata?: { handles?: HandleChannel; handlesLate?: HandleChannel };
241
295
  }>(flight);
242
296
  let hydration: HandleData = {};
@@ -248,12 +302,92 @@ async function decodePayloadHandles(
248
302
  return { hydration: await resolveDeferredHandleValues(hydration), late };
249
303
  }
250
304
 
305
+ /**
306
+ * Each router's Prerender artifacts by manifest key, as `vite build` bakes
307
+ * them (router.matchForPrerender), baked on a URL's first request the way the
308
+ * dev server's /__rsc_prerender endpoint bakes them (`devMode`: a Passthrough
309
+ * route keeps getParams()). `baked` holds the pathnames already tried, one
310
+ * that baked nothing (ctx.passthrough()) included.
311
+ */
312
+ let prerenderArtifacts = new WeakMap<
313
+ object,
314
+ { payloads: Map<string, string>; baked: Set<string> }
315
+ >();
316
+
317
+ /**
318
+ * Serve a Prerender route's handler layer from its build-time artifact, as
319
+ * production does: through the store the built worker reads
320
+ * (createPrerenderStore over globalThis.__loadPrerenderManifestModule,
321
+ * installed as shell-prerender-phase.ts installs it for build-time shells).
322
+ * `env` stands in for the build's `buildEnv`. Returns the undo, or undefined
323
+ * when `pathname` matches no Prerender route.
324
+ */
325
+ async function servePrerenderArtifacts(
326
+ router: Rango<any, any>,
327
+ pathname: string,
328
+ env: unknown,
329
+ ): Promise<(() => void) | undefined> {
330
+ const internal = toInternal(router);
331
+ const matched = await internal.findMatch(pathname);
332
+ // A build bakes an on-demand route only for the params getParams() lists;
333
+ // a refresh-only page serves from its prerender store alone. A Passthrough
334
+ // route is baked here like a build bakes it: the `devMode` render below
335
+ // asks its getParams() and declines a param it does not list. A plain
336
+ // on-demand route is not: nothing here asks its getParams(), and baking
337
+ // every requested param would hide the 404 an unbaked one answers.
338
+ if (!matched?.pr || (matched.od && !matched.pt)) return undefined;
339
+ let artifacts = prerenderArtifacts.get(router);
340
+ if (!artifacts) {
341
+ prerenderArtifacts.set(
342
+ router,
343
+ (artifacts = { payloads: new Map(), baked: new Set() }),
344
+ );
345
+ }
346
+ const { payloads, baked } = artifacts;
347
+ if (!baked.has(pathname)) {
348
+ const result = await internal.matchForPrerender(
349
+ pathname,
350
+ {},
351
+ undefined,
352
+ matched.pt === true,
353
+ env,
354
+ true,
355
+ );
356
+ const { hashParams } = await import("../prerender/param-hash.js");
357
+ baked.add(pathname);
358
+ if (result && !result.passthrough) {
359
+ payloads.set(
360
+ `${result.routeName}/${hashParams(result.params)}`,
361
+ JSON.stringify({ segments: result.segments, handles: result.handles }),
362
+ );
363
+ }
364
+ }
365
+ const [{ createPrerenderStore }, { setPrerenderStoreForTests }] =
366
+ await Promise.all([
367
+ import("../prerender/store.js"),
368
+ import("../router/match-middleware/cache-lookup.js"),
369
+ ]);
370
+ const previousLoader = globalThis.__loadPrerenderManifestModule;
371
+ globalThis.__loadPrerenderManifestModule = async () => ({
372
+ default: Object.fromEntries([...payloads.keys()].map((key) => [key, key])),
373
+ loadPrerenderAsset: async (key) => ({
374
+ default: JSON.parse(payloads.get(key)!),
375
+ }),
376
+ });
377
+ const previousStore = setPrerenderStoreForTests(createPrerenderStore());
378
+ return () => {
379
+ globalThis.__loadPrerenderManifestModule = previousLoader;
380
+ setPrerenderStoreForTests(previousStore);
381
+ };
382
+ }
383
+
251
384
  /**
252
385
  * Handlers per router, keyed by the `cacheStore` override or the router. A
253
- * handler resolves its router's versions when it is created, so an entry is
254
- * reused only while the router would still resolve the versions it was made
255
- * with: a setBuildVersions() call (a simulated deploy) that changes them gets
256
- * a new handler.
386
+ * handler binds its router's document version when it is created, so an entry
387
+ * is reused only while the router would still resolve that version: a
388
+ * setBuildVersions() call (a simulated deploy) that changes it gets a new
389
+ * handler. The data version is read per request, so a data-only change reuses
390
+ * the handler, as a long-lived worker would.
257
391
  */
258
392
  const handlers = new WeakMap<
259
393
  object,
@@ -273,11 +407,10 @@ async function getHandler(
273
407
  const { resolveRouterVersions } =
274
408
  await import("../server/build-version-table.js");
275
409
  // What createRSCHandler would resolve for this router now.
276
- const { data, document } = resolveRouterVersions(
410
+ const { document: versions } = resolveRouterVersions(
277
411
  internal.id,
278
412
  internal.version,
279
413
  );
280
- const versions = `${data}\0${document}`;
281
414
  const cached = byStore.get(cacheStore ?? router);
282
415
  if (cached?.versions === versions) return cached.handler;
283
416
 
@@ -305,6 +438,18 @@ async function getHandler(
305
438
  function buildRequest(url: URL, options: ServeShellRequestOptions): Request {
306
439
  const headers = new Headers(options.headers);
307
440
  const partial = options.partial;
441
+ if (options.form) {
442
+ if (partial) {
443
+ throw new Error(
444
+ "serveShellRequest: form cannot be combined with partial",
445
+ );
446
+ }
447
+ return new Request(url, {
448
+ method: "POST",
449
+ headers,
450
+ body: options.form,
451
+ });
452
+ }
308
453
  if (!partial) {
309
454
  if (!headers.has("accept")) headers.set("accept", "text/html");
310
455
  return new Request(url, { headers });
@@ -327,25 +472,30 @@ function buildRequest(url: URL, options: ServeShellRequestOptions): Request {
327
472
  * starts as a fresh worker would: the capture's stampede guard, its backoff
328
473
  * (a refused capture backs its URL off for later tests too), the shell path's
329
474
  * once-per-key warnings, the build-shell manifest memo, the "use cache"
330
- * in-flight leaders and warnings, and CFCacheStore's isolate memos (shells,
475
+ * in-flight leaders and warnings, CFCacheStore's isolate memos (shells,
331
476
  * tag markers, tag hints) and warnings, which every CFCacheStore in the
332
- * process shares by namespace and URL. Call it in `beforeEach`, never while
333
- * a request is in flight. VercelCacheStore's memos live on the `cache`
334
- * handle it is given: a new handle starts empty.
477
+ * process shares by namespace and URL, and the Prerender artifacts baked so
478
+ * far. Call it in `beforeEach`, never while a request is in flight.
479
+ * VercelCacheStore's memos live on the `cache` handle it is given: a new
480
+ * handle starts empty.
335
481
  */
336
482
  export async function resetShellTestState(): Promise<void> {
337
- const [capture, serve, buildShells, cf, cacheRuntime] = await Promise.all([
338
- import("../rsc/shell-capture.js"),
339
- import("../rsc/shell-serve.js"),
340
- import("../rsc/shell-build-manifest.js"),
341
- import("../cache/cf/cf-cache-store.js"),
342
- import("../cache/cache-runtime.js"),
343
- ]);
483
+ prerenderArtifacts = new WeakMap();
484
+ const [capture, serve, buildShells, cf, cacheRuntime, cacheLookup] =
485
+ await Promise.all([
486
+ import("../rsc/shell-capture.js"),
487
+ import("../rsc/shell-serve.js"),
488
+ import("../rsc/shell-build-manifest.js"),
489
+ import("../cache/cf/cf-cache-store.js"),
490
+ import("../cache/cache-runtime.js"),
491
+ import("../router/match-middleware/cache-lookup.js"),
492
+ ]);
344
493
  capture.resetShellCaptureStateForTests();
345
494
  serve.resetShellServeStateForTests();
346
495
  buildShells.resetBuildShellManifestForTests();
347
496
  cf.resetCFShellMemoForTests();
348
497
  cacheRuntime.resetCacheRuntimeForTests();
498
+ cacheLookup.resetOverlayRevalidationsForTests();
349
499
  }
350
500
 
351
501
  /** Settle background tasks, including ones scheduled while settling. */
@@ -387,19 +537,15 @@ export async function serveShellRequest<TEnv = any>(
387
537
  await ensureRouterManifest(router.id);
388
538
  const handler = await getHandler(router, options.cacheStore);
389
539
  const request = buildRequest(target, options);
390
- const tasks: Promise<unknown>[] = [];
391
- const executionContext: ExecutionContext = {
392
- waitUntil(promise) {
393
- tasks.push(
394
- Promise.resolve(promise).catch((error) =>
395
- console.error("[waitUntil] Background task failed:", error),
396
- ),
397
- );
398
- },
399
- passThroughOnException() {},
400
- };
540
+ const executionContext = createCollectingExecutionContext();
541
+ const { tasks } = executionContext;
401
542
  const recorder: Recorder = {};
402
- while (Date.now() === called) await macrotask();
543
+ const unservePrerender = await servePrerenderArtifacts(
544
+ router,
545
+ target.pathname,
546
+ options.env,
547
+ );
548
+ await awaitLaterMillisecond(called);
403
549
 
404
550
  const { response, body } = await recorders.run(recorder, async () => {
405
551
  try {
@@ -410,6 +556,7 @@ export async function serveShellRequest<TEnv = any>(
410
556
  return { response, body: await response.text() };
411
557
  } finally {
412
558
  await settle(tasks);
559
+ unservePrerender?.();
413
560
  }
414
561
  });
415
562
 
@@ -417,7 +564,8 @@ export async function serveShellRequest<TEnv = any>(
417
564
  recorder.cache && recorder.cache.enabled !== false
418
565
  ? recorder.cache
419
566
  : undefined;
420
- const key = recorder.shellKey ?? shellCacheKey(target, config?.searchParams);
567
+ const key =
568
+ recorder.shellKey ?? shellCacheKey(router, target, config?.searchParams);
421
569
  const tail = recorder.tail;
422
570
  const isFlight =
423
571
  recorder.rendered ||
@@ -446,5 +594,13 @@ export async function serveShellRequest<TEnv = any>(
446
594
  async readHandles() {
447
595
  return flight ? decodePayloadHandles(flight) : undefined;
448
596
  },
597
+ formState: recorder.formState,
598
+ async readPayloadFormState() {
599
+ if (!flight) return undefined;
600
+ const payload = await decodePayload<{
601
+ formState?: Promise<unknown>;
602
+ }>(flight);
603
+ return payload.formState;
604
+ },
449
605
  };
450
606
  }
@@ -15,7 +15,7 @@
15
15
  * 2. **Smallest status signals:** `x-rango-shell` (`HIT` | `MISS`) on document
16
16
  * GETs, and `x-rango-ppr-replay` on partial requests to ppr routes. A replay
17
17
  * HIT is reported only when matching consumes the captured segment record.
18
- * Secondary unit signal: `store.getShell(shellCacheKey(url))` after a real
18
+ * Secondary unit signal: `store.getShell(shellCacheKey(router, url))` after a real
19
19
  * capture flush (background `putShell`). There is no Flight flag for shell HIT.
20
20
  *
21
21
  * 3. **A real MISS → capture → HIT in unit tests:** `serveShellRequest`
@@ -82,10 +82,16 @@ export interface ShellCachePartition {
82
82
  }
83
83
 
84
84
  /**
85
- * Shell store key for a document URL: the production `buildShellKey` (host +
86
- * pathname + sorted search + `:shell`) and `partitionShellKey`, both from the
87
- * React-free leaf `rsc/shell-capture-constants.ts`, so the testing barrel never
88
- * imports the shell-serve module (it pulls React).
85
+ * Shell store key of `router` for a document URL: the production
86
+ * `buildShellKey` (router id + host + pathname + sorted search + `:shell`)
87
+ * and `partitionShellKey`, both from the React-free leaf
88
+ * `rsc/shell-capture-constants.ts`, so the testing barrel never imports the
89
+ * shell-serve module (it pulls React).
90
+ *
91
+ * `router` is the router that serves the URL, or anything carrying its `id`
92
+ * (`{ id }` where the router cannot be imported, as in a Playwright test). A
93
+ * shell belongs to one router: two routers serving one host and path keep
94
+ * two entries.
89
95
  *
90
96
  * Accepts a `URL` or an absolute/relative request URL string (relative strings
91
97
  * resolve against `http://localhost`). Pass the router's `cache.searchParams`
@@ -101,13 +107,18 @@ export interface ShellCachePartition {
101
107
  * `{ keys, generated }` (see {@link ShellCachePartition}).
102
108
  */
103
109
  export function shellCacheKey(
110
+ router: { readonly id: string },
104
111
  url: URL | string,
105
112
  searchParams?: CacheSearchParams,
106
113
  partition?: string | readonly string[] | ShellCachePartition,
107
114
  ): string {
108
115
  const resolved =
109
116
  typeof url === "string" ? new URL(url, "http://localhost") : url;
110
- const key = buildShellKey(resolved, compileSearchParamsFilter(searchParams));
117
+ const key = buildShellKey(
118
+ router.id,
119
+ resolved,
120
+ compileSearchParamsFilter(searchParams),
121
+ );
111
122
  if (partition === undefined) return key;
112
123
  const { keys = [], generated = [] }: ShellCachePartition =
113
124
  typeof partition === "string"