@rangojs/router 0.21.0 → 0.22.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.
- package/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- 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
|
package/src/testing/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
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
|
|
538
|
-
useEffect(
|
|
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:
|
|
755
|
-
|
|
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 }, [
|
|
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 && <
|
|
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)
|
|
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 {
|
|
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
|
|
254
|
-
* reused only while the router would still resolve
|
|
255
|
-
*
|
|
256
|
-
* a
|
|
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 {
|
|
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,
|
|
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
|
|
333
|
-
*
|
|
334
|
-
* handle it is given: a new
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
391
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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
|
|
86
|
-
* pathname + sorted search + `:shell`)
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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(
|
|
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"
|