@rsc-kit/core 0.29.10 → 0.30.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/action.d.ts +69 -9
- package/dist/action.js +78 -3
- package/dist/action.js.map +1 -1
- package/dist/conformance.d.ts +1 -0
- package/dist/conformance.js +24 -0
- package/dist/conformance.js.map +1 -1
- package/dist/headers.d.ts +0 -5
- package/dist/headers.js +0 -5
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +3 -8
- package/dist/host.js +21 -73
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +7 -0
- package/dist/hostCalls.js +7 -1
- package/dist/hostCalls.js.map +1 -1
- package/dist/inlineFlight.d.ts +9 -0
- package/dist/inlineFlight.js +126 -0
- package/dist/inlineFlight.js.map +1 -0
- package/dist/js/Form.d.ts +9 -2
- package/dist/js/Form.js +11 -2
- package/dist/js/Form.js.map +1 -1
- package/dist/js/SegmentBoundary.js +4 -14
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +5 -8
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/inlinePayload.d.ts +6 -0
- package/dist/js/inlinePayload.js +56 -0
- package/dist/js/inlinePayload.js.map +1 -0
- package/dist/js/pagePayload.d.ts +1 -1
- package/dist/js/pagePayload.js +2 -2
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/js/useAction.d.ts +9 -6
- package/dist/js/useAction.js.map +1 -1
- package/dist/testHost.d.ts +6 -2
- package/dist/testHost.js +6 -2
- package/dist/testHost.js.map +1 -1
- package/dist/vite.js +61 -24
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/bootPayloads.d.ts +0 -18
- package/dist/bootPayloads.js +0 -81
- package/dist/bootPayloads.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The payload a document carries for itself - see ../inlineFlight.ts.
|
|
2
|
+
//
|
|
3
|
+
// The server writes each chunk of the render's payload into the document as a
|
|
4
|
+
// script, `self.__rsc_f.push(chunk)`, and a null once it has finished. This
|
|
5
|
+
// turns those into the stream the runtime decodes, as if it had fetched it:
|
|
6
|
+
// what arrived before the runtime ran, then each chunk as it is parsed.
|
|
7
|
+
/**
|
|
8
|
+
* The document's own payload as a response, or null when it carries none -
|
|
9
|
+
* a page from a file the build wrote, a server that writes none - and the
|
|
10
|
+
* runtime fetches it, as it always did.
|
|
11
|
+
*/
|
|
12
|
+
export function inlinePayload() {
|
|
13
|
+
const w = window;
|
|
14
|
+
const queue = w.__rsc_f;
|
|
15
|
+
if (!queue || typeof queue.push !== "function")
|
|
16
|
+
return null;
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const bytes = (chunk) => typeof chunk === "string"
|
|
19
|
+
? encoder.encode(chunk)
|
|
20
|
+
: Uint8Array.from(atob(chunk.b), (c) => c.charCodeAt(0));
|
|
21
|
+
const body = new ReadableStream({
|
|
22
|
+
start(controller) {
|
|
23
|
+
let ended = false;
|
|
24
|
+
const take = (chunk) => {
|
|
25
|
+
if (ended)
|
|
26
|
+
return;
|
|
27
|
+
if (chunk === null) {
|
|
28
|
+
ended = true;
|
|
29
|
+
controller.close();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
controller.enqueue(bytes(chunk));
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0; i < queue.length; i++)
|
|
35
|
+
take(queue[i]);
|
|
36
|
+
queue.length = 0;
|
|
37
|
+
queue.push = (chunk) => {
|
|
38
|
+
take(chunk);
|
|
39
|
+
return 0;
|
|
40
|
+
};
|
|
41
|
+
// A document cut off before the server's end marker: what arrived is
|
|
42
|
+
// all there is, and the decoder is told so rather than left waiting.
|
|
43
|
+
const finished = () => take(null);
|
|
44
|
+
if (document.readyState === "loading")
|
|
45
|
+
document.addEventListener("DOMContentLoaded", finished, { once: true });
|
|
46
|
+
else
|
|
47
|
+
finished();
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const headers = { "Content-Type": "text/x-component", "X-RSC-Segment-Depth": "0" };
|
|
51
|
+
if (typeof w.__rsc_l === "string")
|
|
52
|
+
headers["X-RSC-Layouts"] = w.__rsc_l;
|
|
53
|
+
delete w.__rsc_l;
|
|
54
|
+
return new Response(body, { headers });
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=inlinePayload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inlinePayload.js","sourceRoot":"","sources":["../../src/js/inlinePayload.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,4EAA4E;AAC5E,wEAAwE;AAQxE;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,CAAC,GAAG,MAA0D,CAAC;IACrE,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,CAAC;IAExB,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAE5D,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,CAAC,KAAc,EAAc,EAAE,CAC3C,OAAO,KAAK,KAAK,QAAQ;QACvB,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QACvB,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAE,KAAuB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IAEhF,MAAM,IAAI,GAAG,IAAI,cAAc,CAAa;QAC1C,KAAK,CAAC,UAAU;YACd,IAAI,KAAK,GAAG,KAAK,CAAC;YAClB,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE;gBAC9B,IAAI,KAAK;oBAAE,OAAO;gBAClB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACnB,KAAK,GAAG,IAAI,CAAC;oBACb,UAAU,CAAC,KAAK,EAAE,CAAC;oBAEnB,OAAO;gBACT,CAAC;gBACD,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;YACnC,CAAC,CAAC;YAEF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;gBAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;YACtD,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjB,KAAK,CAAC,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE;gBAC9B,IAAI,CAAC,KAAK,CAAC,CAAC;gBAEZ,OAAO,CAAC,CAAC;YACX,CAAC,CAAC;YAEF,qEAAqE;YACrE,qEAAqE;YACrE,MAAM,QAAQ,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAElC,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS;gBAAE,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;;gBAC1G,QAAQ,EAAE,CAAC;QAClB,CAAC;KACF,CAAC,CAAC;IAEH,MAAM,OAAO,GAA2B,EAAE,cAAc,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,GAAG,EAAE,CAAC;IAE3G,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ;QAAE,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;IAExE,OAAO,CAAC,CAAC,OAAO,CAAC;IAEjB,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;AACzC,CAAC","sourcesContent":["// The payload a document carries for itself - see ../inlineFlight.ts.\n//\n// The server writes each chunk of the render's payload into the document as a\n// script, `self.__rsc_f.push(chunk)`, and a null once it has finished. This\n// turns those into the stream the runtime decodes, as if it had fetched it:\n// what arrived before the runtime ran, then each chunk as it is parsed.\n\ninterface Queue {\n push(chunk: unknown): number;\n length: number;\n [index: number]: unknown;\n}\n\n/**\n * The document's own payload as a response, or null when it carries none -\n * a page from a file the build wrote, a server that writes none - and the\n * runtime fetches it, as it always did.\n */\nexport function inlinePayload(): Response | null {\n const w = window as unknown as { __rsc_f?: Queue; __rsc_l?: string };\n const queue = w.__rsc_f;\n\n if (!queue || typeof queue.push !== \"function\") return null;\n\n const encoder = new TextEncoder();\n const bytes = (chunk: unknown): Uint8Array =>\n typeof chunk === \"string\"\n ? encoder.encode(chunk)\n : Uint8Array.from(atob((chunk as { b: string }).b), (c) => c.charCodeAt(0));\n\n const body = new ReadableStream<Uint8Array>({\n start(controller) {\n let ended = false;\n const take = (chunk: unknown) => {\n if (ended) return;\n if (chunk === null) {\n ended = true;\n controller.close();\n\n return;\n }\n controller.enqueue(bytes(chunk));\n };\n\n for (let i = 0; i < queue.length; i++) take(queue[i]);\n queue.length = 0;\n queue.push = (chunk: unknown) => {\n take(chunk);\n\n return 0;\n };\n\n // A document cut off before the server's end marker: what arrived is\n // all there is, and the decoder is told so rather than left waiting.\n const finished = () => take(null);\n\n if (document.readyState === \"loading\") document.addEventListener(\"DOMContentLoaded\", finished, { once: true });\n else finished();\n },\n });\n\n const headers: Record<string, string> = { \"Content-Type\": \"text/x-component\", \"X-RSC-Segment-Depth\": \"0\" };\n\n if (typeof w.__rsc_l === \"string\") headers[\"X-RSC-Layouts\"] = w.__rsc_l;\n\n delete w.__rsc_l;\n\n return new Response(body, { headers });\n}\n"]}
|
package/dist/js/pagePayload.d.ts
CHANGED
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* a build. Nothing there can be imported by a test, which is how this handling
|
|
12
12
|
* came to have none.
|
|
13
13
|
*/
|
|
14
|
-
export declare function fetchPagePayload(url: string, fetchImpl?: typeof fetch
|
|
14
|
+
export declare function fetchPagePayload(url: string, fetchImpl?: typeof fetch): Promise<Response>;
|
package/dist/js/pagePayload.js
CHANGED
|
@@ -13,10 +13,10 @@ import { reportReachable } from "./onlineStore";
|
|
|
13
13
|
* a build. Nothing there can be imported by a test, which is how this handling
|
|
14
14
|
* came to have none.
|
|
15
15
|
*/
|
|
16
|
-
export async function fetchPagePayload(url, fetchImpl = fetch
|
|
16
|
+
export async function fetchPagePayload(url, fetchImpl = fetch) {
|
|
17
17
|
let response;
|
|
18
18
|
try {
|
|
19
|
-
response = await fetchImpl(url, { headers: { "X-RSC": "1"
|
|
19
|
+
response = await fetchImpl(url, { headers: { "X-RSC": "1" } });
|
|
20
20
|
}
|
|
21
21
|
catch (err) {
|
|
22
22
|
// Nothing answered, so the app is offline as far as the router is
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pagePayload.js","sourceRoot":"","sources":["../../src/js/pagePayload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,SAAS,GAAiB,KAAK
|
|
1
|
+
{"version":3,"file":"pagePayload.js","sourceRoot":"","sources":["../../src/js/pagePayload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,SAAS,GAAiB,KAAK;IAE/B,IAAI,QAAkB,CAAC;IAEvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,kEAAkE;QAClE,8CAA8C;QAC9C,eAAe,CAAC,KAAK,CAAC,CAAC;QACvB,mBAAmB,CAAC,kCAAkC,EAAE,GAAG,CAAC,CAAC;QAE7D,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,wCAAwC;IACxC,eAAe,CAAC,IAAI,CAAC,CAAC;IAEtB,IAAI,CAAC;QACH,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,mBAAmB,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC;QAElE,MAAM,GAAG,CAAC;IACZ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC","sourcesContent":["import { reportClientFailure, throwForFailedPayload } from \"./errors\";\nimport { reportReachable } from \"./onlineStore\";\n\n/**\n * Fetch the payload the page hydrates from, and account for it failing.\n *\n * This is the one request with nothing watching it. A PPR route's shell is\n * real HTML with a 200, so a page whose payload never arrives looks like it\n * loaded and then sits on its Suspense fallbacks indefinitely — no error, no\n * fallback, nothing an app can react to.\n *\n * Kept apart from createViteRscApp because that module imports the Flight\n * browser runtime, which reaches a Vite virtual module that exists only inside\n * a build. Nothing there can be imported by a test, which is how this handling\n * came to have none.\n */\nexport async function fetchPagePayload(\n url: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<Response> {\n let response: Response;\n\n try {\n response = await fetchImpl(url, { headers: { \"X-RSC\": \"1\" } });\n } catch (err) {\n // Nothing answered, so the app is offline as far as the router is\n // concerned — which is what useOffline reads.\n reportReachable(false);\n reportClientFailure(\"could not fetch the page payload\", err);\n\n throw err;\n }\n\n // Something answered, whatever it said.\n reportReachable(true);\n\n try {\n throwForFailedPayload(response);\n } catch (err) {\n reportClientFailure(\"the server could not render this page\", err);\n\n throw err;\n }\n\n return response;\n}\n"]}
|
package/dist/js/useAction.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ActionResult } from "../action.js";
|
|
2
|
-
type AnyAction = (...args: any[]) => Promise<ActionResult<any>>;
|
|
3
|
-
type DataOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<infer D> ? D : never;
|
|
2
|
+
type AnyAction = (...args: any[]) => Promise<ActionResult<any, any>>;
|
|
3
|
+
type DataOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<infer D, any> ? D : never;
|
|
4
|
+
type RefusalOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<any, infer R> ? R : never;
|
|
5
|
+
/** The whole result, refusal type included - see `.refusal(schema)`. */
|
|
6
|
+
type ResultOf<A extends AnyAction> = ActionResult<DataOf<A>, RefusalOf<A>>;
|
|
4
7
|
export type UseActionStatus = "idle" | "executing" | "hasSucceeded" | "hasErrored";
|
|
5
8
|
export interface UseActionOptions<A extends AnyAction> {
|
|
6
9
|
/**
|
|
@@ -17,9 +20,9 @@ export interface UseActionOptions<A extends AnyAction> {
|
|
|
17
20
|
* validationErrors, the first one - so a button with no field beside it
|
|
18
21
|
* still has something to show.
|
|
19
22
|
*/
|
|
20
|
-
onError?: (message: string, result:
|
|
23
|
+
onError?: (message: string, result: ResultOf<A>) => void;
|
|
21
24
|
/** After either, for state that tracks the call rather than its outcome. */
|
|
22
|
-
onSettled?: (result:
|
|
25
|
+
onSettled?: (result: ResultOf<A>) => void;
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
28
|
* Call an action from a button rather than a form.
|
|
@@ -46,10 +49,10 @@ export interface UseActionOptions<A extends AnyAction> {
|
|
|
46
49
|
*/
|
|
47
50
|
export declare function useAction<A extends AnyAction>(action: A, options?: UseActionOptions<A>): {
|
|
48
51
|
execute: (...args: Parameters<A>) => void;
|
|
49
|
-
executeAsync: (...args: Parameters<A>) => Promise<
|
|
52
|
+
executeAsync: (...args: Parameters<A>) => Promise<ResultOf<A>>;
|
|
50
53
|
isPending: boolean;
|
|
51
54
|
status: UseActionStatus;
|
|
52
|
-
result:
|
|
55
|
+
result: ResultOf<A>;
|
|
53
56
|
reset: () => void;
|
|
54
57
|
};
|
|
55
58
|
export default useAction;
|
package/dist/js/useAction.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAction.js","sourceRoot":"","sources":["../../src/js/useAction.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useAction.js","sourceRoot":"","sources":["../../src/js/useAction.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef, useState, useTransition } from \"react\";\nimport type { ActionResult } from \"../action.js\";\n\n// Any built action: createActionClient's handler(), with or without a schema.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyAction = (...args: any[]) => Promise<ActionResult<any, any>>;\n\ntype DataOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<infer D, any> ? D : never;\n\ntype RefusalOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<any, infer R> ? R : never;\n\n/** The whole result, refusal type included - see `.refusal(schema)`. */\ntype ResultOf<A extends AnyAction> = ActionResult<DataOf<A>, RefusalOf<A>>;\n\nexport type UseActionStatus = \"idle\" | \"executing\" | \"hasSucceeded\" | \"hasErrored\";\n\nexport interface UseActionOptions<A extends AnyAction> {\n /**\n * Called inside the action's transition, before the action runs, with what\n * it was called with - where React's `useOptimistic` setter goes. React\n * puts the optimistic state back when the transition ends, so a failed\n * action takes its change back on its own.\n */\n optimistic?: (...args: Parameters<A>) => void;\n /** The action answered with data: no serverError, no validationErrors. */\n onSuccess?: (data: DataOf<A>) => void;\n /**\n * The action refused or failed. With a serverError, that message; with\n * validationErrors, the first one - so a button with no field beside it\n * still has something to show.\n */\n onError?: (message: string, result: ResultOf<A>) => void;\n /** After either, for state that tracks the call rather than its outcome. */\n onSettled?: (result: ResultOf<A>) => void;\n}\n\n/**\n * Call an action from a button rather than a form.\n *\n * const { execute, isPending } = useAction(archivePost, {\n * optimistic: (input) => hide(input.id),\n * onError: (message) => toast.error(message),\n * })\n *\n * <button disabled={isPending} onClick={() => execute({ id })}>Archive</button>\n *\n * The action's own types carry through: `execute` takes what the action's\n * schema takes, and `onSuccess` is handed what its handler returns. A form does\n * not need this - <Form> reads the same result on its own.\n *\n * Neither `execute` nor `executeAsync` throws. An action built on the action\n * client returns its failures, and anything thrown on the way - the network,\n * a server that answered with a page - becomes a serverError: left thrown,\n * the transition never settled and an optimistic change stayed on screen as\n * though the write had worked.\n *\n * An action that redirected settles nothing: no callback runs and no state is\n * set, because the page is on its way somewhere else.\n */\nexport function useAction<A extends AnyAction>(action: A, options: UseActionOptions<A> = {}) {\n type Result = ResultOf<A>;\n\n const [isPending, startTransition] = useTransition();\n const [result, setResult] = useState<Result>({});\n const [settled, setSettled] = useState<\"hasSucceeded\" | \"hasErrored\" | null>(null);\n\n // Read when the action runs, not when execute was made: execute keeps one\n // identity across renders, so an onClick built on it is not rebuilt, and a\n // callback closing over newer state is still the one called.\n const latest = useRef({ action, options });\n\n useLayoutEffect(() => {\n latest.current = { action, options };\n });\n\n const run = useCallback(async (args: Parameters<A>): Promise<Result> => {\n const { action, options } = latest.current;\n\n options.optimistic?.(...args);\n\n let next: Result;\n\n try {\n next = (await action(...args)) as Result;\n } catch (error) {\n next = { serverError: error instanceof Error ? error.message : String(error) };\n }\n\n if (next.redirected !== undefined) return next;\n\n setResult(next);\n setSettled(next.serverError !== undefined || next.validationErrors ? \"hasErrored\" : \"hasSucceeded\");\n\n if (next.serverError !== undefined) {\n options.onError?.(next.serverError, next);\n } else if (next.validationErrors) {\n options.onError?.(Object.values(next.validationErrors).flat()[0] ?? \"Invalid input.\", next);\n } else {\n options.onSuccess?.(next.data as DataOf<A>);\n }\n\n options.onSettled?.(next);\n\n return next;\n }, []);\n\n const executeAsync = useCallback(\n (...args: Parameters<A>): Promise<Result> =>\n new Promise((resolve) => {\n startTransition(async () => {\n resolve(await run(args));\n });\n }),\n [run],\n );\n\n const execute = useCallback(\n (...args: Parameters<A>): void => {\n void executeAsync(...args);\n },\n [executeAsync],\n );\n\n const reset = useCallback(() => {\n setResult({});\n setSettled(null);\n }, []);\n\n const status: UseActionStatus = isPending ? \"executing\" : (settled ?? \"idle\");\n\n return { execute, executeAsync, isPending, status, result, reset };\n}\n\nexport default useAction;\n"]}
|
package/dist/testHost.d.ts
CHANGED
|
@@ -102,8 +102,12 @@ export declare const hostReply: {
|
|
|
102
102
|
readonly unauthorized: (message?: string) => Reply;
|
|
103
103
|
/** Send the visitor somewhere else, as the host's redirect does. */
|
|
104
104
|
readonly redirect: (to: string, status?: number) => Reply;
|
|
105
|
-
/**
|
|
106
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Refuse with a status - a 404 for a record this caller cannot see, a 429 -
|
|
107
|
+
* and, optionally, data for the page to act on: what is blocking a delete.
|
|
108
|
+
* With data it reaches an action as `refuse()` does, as `result.refusal`.
|
|
109
|
+
*/
|
|
110
|
+
readonly refuse: (status: number, message?: string, data?: unknown) => Reply;
|
|
107
111
|
/** The input was refused, by field: an action returns these as validationErrors. */
|
|
108
112
|
readonly invalid: (errors: Record<string, string[]>) => Reply;
|
|
109
113
|
/** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */
|
package/dist/testHost.js
CHANGED
|
@@ -40,8 +40,12 @@ export const hostReply = {
|
|
|
40
40
|
unauthorized: (message) => reply({ unauthorized: true, error: message }),
|
|
41
41
|
/** Send the visitor somewhere else, as the host's redirect does. */
|
|
42
42
|
redirect: (to, status = 307) => reply({ redirect: to, redirectStatus: status }),
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Refuse with a status - a 404 for a record this caller cannot see, a 429 -
|
|
45
|
+
* and, optionally, data for the page to act on: what is blocking a delete.
|
|
46
|
+
* With data it reaches an action as `refuse()` does, as `result.refusal`.
|
|
47
|
+
*/
|
|
48
|
+
refuse: (status, message = 'Refused.', data) => reply(data === undefined ? { error: message, refusalStatus: status } : { error: message, refusalStatus: status, refusalData: data }),
|
|
45
49
|
/** The input was refused, by field: an action returns these as validationErrors. */
|
|
46
50
|
invalid: (errors) => reply({ validationErrors: errors }),
|
|
47
51
|
/** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */
|
package/dist/testHost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testHost.js","sourceRoot":"","sources":["../src/testHost.ts"],"names":[],"mappings":"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","sourcesContent":["// A backend for tests: answers rpc() calls and host middleware the way a Go\n// or Laravel host answers them over HTTP, without one running.\n//\n// The app under test talks to it through the real client, httpHostCalls,\n// handed a fetch that answers in-process. So a test exercises what ships -\n// batching, a redirect raised as the engine's own, an unauthenticated reply\n// becoming a 401 - and only the backend is stood in for.\n\nimport type { HostCallReply } from './hostCalls.js'\nimport { CHANGED_FUNCTION, memoryVersions } from './changed.js'\nimport type { ChangedAnswer, ChangedQuery } from './changed.js'\n\nconst REPLY = Symbol.for('rsc-kit.test-host-reply')\n/** Cookies a reply sets: sent as Set-Cookie headers, never in the body. */\nconst COOKIES = Symbol.for('rsc-kit.test-host-cookies')\n\n/** The name the engine asks a host's route guards under. */\nexport const HOST_MIDDLEWARE = '__rsc.middleware'\n\n/**\n * Tag versions for a test host, kept in the test.\n *\n * const names = testChanges()\n * const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })\n * names.changed('team:1:repos') // what the backend's webhook would say\n *\n * `host` answers `__rsc.changed` the way an adapter does - holding the call up\n * to `wait` for a name to move - and `changed` moves one.\n */\n/**\n * The test host's answer to `__rsc.changed`: given what any test host is\n * given, answering a `ChangedAnswer`.\n *\n * Both halves matter. It takes `HostCallInput`, so an untyped host of\n * `HostHandler`s accepts it; it returns a `ChangedAnswer`, so a host typed\n * from the app's rsc-host.json - whose `__rsc.changed` must answer one -\n * accepts it too. Typed as `HostHandler`, it returned `unknown`, and\n * spreading it into a typed host did not compile.\n */\nexport interface ChangedHost {\n [CHANGED_FUNCTION]: (call: HostCallInput) => Promise<ChangedAnswer>\n}\n\nexport function testChanges(): { host: ChangedHost; changed: (...names: string[]) => void } {\n const source = memoryVersions()\n\n return {\n host: {\n [CHANGED_FUNCTION]: async ({ args }) => {\n const query = args[0] as Partial<ChangedQuery> | undefined\n\n return { versions: await source.changed(query?.since ?? {}, Math.min(query?.wait ?? 0, 5_000)) }\n },\n },\n changed: (...names) => void source.bump(names),\n }\n}\n\n/** What a test handler is given: the call's arguments, and the headers the renderer forwarded. */\nexport interface HostCallInput {\n args: unknown[]\n /** The visitor's request headers the renderer passed on - the cookie, the Accept-Language. */\n headers: Headers\n}\n\n/** Answers one function. Return a value for its result, or one of `hostReply`'s answers. */\nexport type HostHandler = (call: HostCallInput) => unknown\n\ntype Reply = Omit<HostCallReply, 'result'> & { [REPLY]: true }\n\n/** One of the protocol's own answers, from `hostReply`. */\nexport type HostReply = Reply\n\ndeclare global {\n /**\n * The backend's functions, as the build read them from rsc-host.json:\n * each one's arguments and result. Written into .rsc-kit/rsc-env.d.ts;\n * empty without a manifest.\n */\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface RscHostFunctions {}\n}\n\ntype Answer<T> = T | HostReply | Promise<T | HostReply>\n\ntype Guards = {\n [HOST_MIDDLEWARE]?: (call: { args: [names: string[]]; headers: Headers }) => Answer<true>\n [CHANGED_FUNCTION]?: (call: { args: [query: ChangedQuery]; headers: Headers }) => Answer<ChangedAnswer>\n}\n\n/**\n * The backend's functions, by the name rpc() calls them with.\n *\n * Typed from the app's rsc-host.json when it has one: a handler may only\n * answer for a function the backend has, is given its arguments, and returns\n * its result or one of `hostReply`'s answers - never a shape the backend\n * would not send, which is how a fake drifts from the real thing while its\n * tests stay green. Without a manifest, any name and any value.\n *\n * Route guards declared for the host are asked as `__rsc.middleware`\n * (`HOST_MIDDLEWARE`), with the guard names as the first argument; answer\n * `true` to let the page render.\n */\nexport type TestHost = [keyof RscHostFunctions] extends [never]\n ? Record<string, HostHandler>\n : {\n [K in keyof RscHostFunctions]?: RscHostFunctions[K] extends { args: infer A extends unknown[]; result: infer R }\n ? (call: { args: A; headers: Headers }) => Answer<R>\n : never\n } & Guards\n\nconst reply = (fields: Omit<Reply, typeof REPLY>): Reply => ({ ...fields, [REPLY]: true })\n\n/**\n * The protocol's own answers, for a handler to return.\n *\n * host: {\n * 'Apps.list': ({ headers }) =>\n * headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),\n * 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),\n * }\n */\nexport const hostReply = {\n /** No session: the page answers 401, an action its auth error. */\n unauthenticated: (message?: string): Reply => reply({ unauthenticated: true, error: message }),\n /** A session that still may not: 403. */\n unauthorized: (message?: string): Reply => reply({ unauthorized: true, error: message }),\n /** Send the visitor somewhere else, as the host's redirect does. */\n redirect: (to: string, status = 307): Reply => reply({ redirect: to, redirectStatus: status }),\n /** Refuse with a status - a 404 for a record this caller cannot see, a 429. */\n refuse: (status: number, message = 'Refused.'): Reply => reply({ error: message, refusalStatus: status }),\n /** The input was refused, by field: an action returns these as validationErrors. */\n invalid: (errors: Record<string, string[]>): Reply => reply({ validationErrors: errors }),\n /** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */\n fail: (message = 'Something went wrong.'): Reply => reply({ error: message }),\n /** A result, and the regions of the page the call says it changed. */\n revalidating: (result: unknown, ...targets: string[]): Reply =>\n reply({ result, revalidate: targets } as Omit<Reply, typeof REPLY>),\n /** A result, and cookies set on the response - a login's session: `'session=abc; Path=/; HttpOnly'`. */\n settingCookies: (result: unknown, ...cookies: string[]): Reply =>\n reply({ result, [COOKIES]: cookies } as unknown as Omit<Reply, typeof REPLY>),\n} as const\n\nfunction isReply(value: unknown): value is Reply {\n return typeof value === 'object' && value !== null && (value as Record<symbol, unknown>)[REPLY] === true\n}\n\nasync function answer(host: TestHost, name: string, args: unknown[], headers: Headers): Promise<HostCallReply> {\n // By a name only known at runtime: a typed host has no index signature.\n const handler = (host as Record<string, HostHandler | undefined>)[name]\n\n // Fails the call rather than answering null: a function the test forgot\n // is a test that would otherwise pass against nothing.\n if (!handler) {\n return {\n error:\n name === HOST_MIDDLEWARE\n ? `the page asked its host guards (${JSON.stringify(args[0])}) and the test host has no \"${HOST_MIDDLEWARE}\" handler`\n : `the test host has no handler for ${JSON.stringify(name)}`,\n }\n }\n\n const value = await handler({ args, headers })\n\n if (isReply(value)) {\n // Symbol keys survive the rest spread; COOKIES is read off by the fetch\n // below, and JSON leaves it out of the body.\n const { [REPLY]: _, ...fields } = value\n\n return fields\n }\n\n return { result: value === undefined ? null : value }\n}\n\n/**\n * A fetch that answers the host-call endpoint from `host`: one call as\n * itself, a batch as `{ replies }`, in order.\n */\nexport function testHostFetch(host: TestHost): typeof fetch {\n return (async (_input: RequestInfo | URL, init?: RequestInit) => {\n const headers = new Headers(init?.headers)\n const body = JSON.parse(String(init?.body ?? '{}')) as\n | { function: string; args?: unknown[] }\n | { calls: { function: string; args?: unknown[] }[] }\n\n // The renderer's own headers are not the visitor's.\n headers.delete('x-rsc-host-secret')\n headers.delete('content-type')\n\n const json = (value: unknown) =>\n new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } })\n\n if ('calls' in body) {\n return json({\n replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),\n })\n }\n\n const single = await answer(host, body.function, body.args ?? [], headers)\n const cookies = (single as { [COOKIES]?: string[] })[COOKIES] ?? []\n const response = json(single)\n\n for (const cookie of cookies) response.headers.append('Set-Cookie', cookie)\n\n return response\n }) as typeof fetch\n}\n"]}
|
|
1
|
+
{"version":3,"file":"testHost.js","sourceRoot":"","sources":["../src/testHost.ts"],"names":[],"mappings":"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","sourcesContent":["// A backend for tests: answers rpc() calls and host middleware the way a Go\n// or Laravel host answers them over HTTP, without one running.\n//\n// The app under test talks to it through the real client, httpHostCalls,\n// handed a fetch that answers in-process. So a test exercises what ships -\n// batching, a redirect raised as the engine's own, an unauthenticated reply\n// becoming a 401 - and only the backend is stood in for.\n\nimport type { HostCallReply } from './hostCalls.js'\nimport { CHANGED_FUNCTION, memoryVersions } from './changed.js'\nimport type { ChangedAnswer, ChangedQuery } from './changed.js'\n\nconst REPLY = Symbol.for('rsc-kit.test-host-reply')\n/** Cookies a reply sets: sent as Set-Cookie headers, never in the body. */\nconst COOKIES = Symbol.for('rsc-kit.test-host-cookies')\n\n/** The name the engine asks a host's route guards under. */\nexport const HOST_MIDDLEWARE = '__rsc.middleware'\n\n/**\n * Tag versions for a test host, kept in the test.\n *\n * const names = testChanges()\n * const app = await createTestApp({ host: { ...names.host, 'Repos.list': () => repos } })\n * names.changed('team:1:repos') // what the backend's webhook would say\n *\n * `host` answers `__rsc.changed` the way an adapter does - holding the call up\n * to `wait` for a name to move - and `changed` moves one.\n */\n/**\n * The test host's answer to `__rsc.changed`: given what any test host is\n * given, answering a `ChangedAnswer`.\n *\n * Both halves matter. It takes `HostCallInput`, so an untyped host of\n * `HostHandler`s accepts it; it returns a `ChangedAnswer`, so a host typed\n * from the app's rsc-host.json - whose `__rsc.changed` must answer one -\n * accepts it too. Typed as `HostHandler`, it returned `unknown`, and\n * spreading it into a typed host did not compile.\n */\nexport interface ChangedHost {\n [CHANGED_FUNCTION]: (call: HostCallInput) => Promise<ChangedAnswer>\n}\n\nexport function testChanges(): { host: ChangedHost; changed: (...names: string[]) => void } {\n const source = memoryVersions()\n\n return {\n host: {\n [CHANGED_FUNCTION]: async ({ args }) => {\n const query = args[0] as Partial<ChangedQuery> | undefined\n\n return { versions: await source.changed(query?.since ?? {}, Math.min(query?.wait ?? 0, 5_000)) }\n },\n },\n changed: (...names) => void source.bump(names),\n }\n}\n\n/** What a test handler is given: the call's arguments, and the headers the renderer forwarded. */\nexport interface HostCallInput {\n args: unknown[]\n /** The visitor's request headers the renderer passed on - the cookie, the Accept-Language. */\n headers: Headers\n}\n\n/** Answers one function. Return a value for its result, or one of `hostReply`'s answers. */\nexport type HostHandler = (call: HostCallInput) => unknown\n\ntype Reply = Omit<HostCallReply, 'result'> & { [REPLY]: true }\n\n/** One of the protocol's own answers, from `hostReply`. */\nexport type HostReply = Reply\n\ndeclare global {\n /**\n * The backend's functions, as the build read them from rsc-host.json:\n * each one's arguments and result. Written into .rsc-kit/rsc-env.d.ts;\n * empty without a manifest.\n */\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface RscHostFunctions {}\n}\n\ntype Answer<T> = T | HostReply | Promise<T | HostReply>\n\ntype Guards = {\n [HOST_MIDDLEWARE]?: (call: { args: [names: string[]]; headers: Headers }) => Answer<true>\n [CHANGED_FUNCTION]?: (call: { args: [query: ChangedQuery]; headers: Headers }) => Answer<ChangedAnswer>\n}\n\n/**\n * The backend's functions, by the name rpc() calls them with.\n *\n * Typed from the app's rsc-host.json when it has one: a handler may only\n * answer for a function the backend has, is given its arguments, and returns\n * its result or one of `hostReply`'s answers - never a shape the backend\n * would not send, which is how a fake drifts from the real thing while its\n * tests stay green. Without a manifest, any name and any value.\n *\n * Route guards declared for the host are asked as `__rsc.middleware`\n * (`HOST_MIDDLEWARE`), with the guard names as the first argument; answer\n * `true` to let the page render.\n */\nexport type TestHost = [keyof RscHostFunctions] extends [never]\n ? Record<string, HostHandler>\n : {\n [K in keyof RscHostFunctions]?: RscHostFunctions[K] extends { args: infer A extends unknown[]; result: infer R }\n ? (call: { args: A; headers: Headers }) => Answer<R>\n : never\n } & Guards\n\nconst reply = (fields: Omit<Reply, typeof REPLY>): Reply => ({ ...fields, [REPLY]: true })\n\n/**\n * The protocol's own answers, for a handler to return.\n *\n * host: {\n * 'Apps.list': ({ headers }) =>\n * headers.get('cookie')?.includes('session=') ? [{ id: 1 }] : hostReply.unauthenticated(),\n * 'Apps.create': ({ args }) => hostReply.invalid({ name: ['Taken.'] }),\n * }\n */\nexport const hostReply = {\n /** No session: the page answers 401, an action its auth error. */\n unauthenticated: (message?: string): Reply => reply({ unauthenticated: true, error: message }),\n /** A session that still may not: 403. */\n unauthorized: (message?: string): Reply => reply({ unauthorized: true, error: message }),\n /** Send the visitor somewhere else, as the host's redirect does. */\n redirect: (to: string, status = 307): Reply => reply({ redirect: to, redirectStatus: status }),\n /**\n * Refuse with a status - a 404 for a record this caller cannot see, a 429 -\n * and, optionally, data for the page to act on: what is blocking a delete.\n * With data it reaches an action as `refuse()` does, as `result.refusal`.\n */\n refuse: (status: number, message = 'Refused.', data?: unknown): Reply =>\n reply(data === undefined ? { error: message, refusalStatus: status } : { error: message, refusalStatus: status, refusalData: data }),\n /** The input was refused, by field: an action returns these as validationErrors. */\n invalid: (errors: Record<string, string[]>): Reply => reply({ validationErrors: errors }),\n /** An unexpected failure, as an adapter answers a crash: an error, not a refusal. */\n fail: (message = 'Something went wrong.'): Reply => reply({ error: message }),\n /** A result, and the regions of the page the call says it changed. */\n revalidating: (result: unknown, ...targets: string[]): Reply =>\n reply({ result, revalidate: targets } as Omit<Reply, typeof REPLY>),\n /** A result, and cookies set on the response - a login's session: `'session=abc; Path=/; HttpOnly'`. */\n settingCookies: (result: unknown, ...cookies: string[]): Reply =>\n reply({ result, [COOKIES]: cookies } as unknown as Omit<Reply, typeof REPLY>),\n} as const\n\nfunction isReply(value: unknown): value is Reply {\n return typeof value === 'object' && value !== null && (value as Record<symbol, unknown>)[REPLY] === true\n}\n\nasync function answer(host: TestHost, name: string, args: unknown[], headers: Headers): Promise<HostCallReply> {\n // By a name only known at runtime: a typed host has no index signature.\n const handler = (host as Record<string, HostHandler | undefined>)[name]\n\n // Fails the call rather than answering null: a function the test forgot\n // is a test that would otherwise pass against nothing.\n if (!handler) {\n return {\n error:\n name === HOST_MIDDLEWARE\n ? `the page asked its host guards (${JSON.stringify(args[0])}) and the test host has no \"${HOST_MIDDLEWARE}\" handler`\n : `the test host has no handler for ${JSON.stringify(name)}`,\n }\n }\n\n const value = await handler({ args, headers })\n\n if (isReply(value)) {\n // Symbol keys survive the rest spread; COOKIES is read off by the fetch\n // below, and JSON leaves it out of the body.\n const { [REPLY]: _, ...fields } = value\n\n return fields\n }\n\n return { result: value === undefined ? null : value }\n}\n\n/**\n * A fetch that answers the host-call endpoint from `host`: one call as\n * itself, a batch as `{ replies }`, in order.\n */\nexport function testHostFetch(host: TestHost): typeof fetch {\n return (async (_input: RequestInfo | URL, init?: RequestInit) => {\n const headers = new Headers(init?.headers)\n const body = JSON.parse(String(init?.body ?? '{}')) as\n | { function: string; args?: unknown[] }\n | { calls: { function: string; args?: unknown[] }[] }\n\n // The renderer's own headers are not the visitor's.\n headers.delete('x-rsc-host-secret')\n headers.delete('content-type')\n\n const json = (value: unknown) =>\n new Response(JSON.stringify(value), { headers: { 'content-type': 'application/json' } })\n\n if ('calls' in body) {\n return json({\n replies: await Promise.all(body.calls.map((c) => answer(host, c.function, c.args ?? [], headers))),\n })\n }\n\n const single = await answer(host, body.function, body.args ?? [], headers)\n const cookies = (single as { [COOKIES]?: string[] })[COOKIES] ?? []\n const response = json(single)\n\n for (const cookie of cookies) response.headers.append('Set-Cookie', cookie)\n\n return response\n }) as typeof fetch\n}\n"]}
|
package/dist/vite.js
CHANGED
|
@@ -3023,10 +3023,9 @@ import { isSharedSection, sectionComponent } from ${JSON.stringify(join(packageD
|
|
|
3023
3023
|
import { shareRender } from ${JSON.stringify(join(packageDir, "sharedRenders"))}
|
|
3024
3024
|
import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
|
|
3025
3025
|
import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
|
|
3026
|
-
import { requestFromCrawler, requestSearch, searchParams as requestSearchParams, withUrl } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3026
|
+
import { requestFromCrawler, requestSearch, searchParams as requestSearchParams, withRequest, withUrl } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3027
3027
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
3028
3028
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
3029
|
-
import { bootToken } from ${JSON.stringify(join(packageDir, "bootPayloads"))}
|
|
3030
3029
|
import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3031
3030
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
3032
3031
|
import { cancelledByConsumer } from ${JSON.stringify(join(packageDir, "js/fallbackReport"))}
|
|
@@ -4538,7 +4537,7 @@ export async function handleRscHtmlStream(
|
|
|
4538
4537
|
nonce?: string,
|
|
4539
4538
|
pageKey = '',
|
|
4540
4539
|
bootstrap = true,
|
|
4541
|
-
): Promise<{ htmlStream: ReadableStream; rscPayload
|
|
4540
|
+
): Promise<{ htmlStream: ReadableStream; rscPayload?: ReadableStream; clientChunks: unknown }> {
|
|
4542
4541
|
await instrumented()
|
|
4543
4542
|
applyHost()
|
|
4544
4543
|
await runMiddleware(component, props)
|
|
@@ -4546,18 +4545,21 @@ export async function handleRscHtmlStream(
|
|
|
4546
4545
|
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
|
|
4547
4546
|
{ onError: flightOnError },
|
|
4548
4547
|
)
|
|
4549
|
-
// Written into the bootstrap script, so it is in the document before the
|
|
4550
|
-
// runtime that reads it. Only where there is a runtime: a page shipping
|
|
4551
|
-
// none never comes back for a payload.
|
|
4552
|
-
const token = bootstrap ? bootToken() : undefined
|
|
4553
|
-
// The same tree twice: one copy becomes the HTML, the other is the payload
|
|
4554
|
-
// the browser comes back for to hydrate. The host holds it for that fetch
|
|
4555
|
-
// - see bootPayloads.ts - so a visit renders once. Streamed, not read to a
|
|
4556
|
-
// string: it used to be buffered whole here and never read by anyone.
|
|
4557
|
-
const [forHtml, rscPayload] = flight.tee()
|
|
4558
4548
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4559
|
-
|
|
4560
|
-
|
|
4549
|
+
|
|
4550
|
+
// A page with no runtime hydrates nothing: straight into the HTML.
|
|
4551
|
+
if (!bootstrap) {
|
|
4552
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler())
|
|
4553
|
+
return { htmlStream, clientChunks: {} }
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
// One render: the HTML from one copy, and from the other the payload the
|
|
4557
|
+
// browser hydrates from, which the host streams into the document - see
|
|
4558
|
+
// inlineFlight.ts. It used to be fetched by the browser, and that fetch
|
|
4559
|
+
// rendered the whole page again.
|
|
4560
|
+
const [forHtml, rscPayload] = flight.tee()
|
|
4561
|
+
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler(), true)
|
|
4562
|
+
return { htmlStream, rscPayload, clientChunks: {} }
|
|
4561
4563
|
}
|
|
4562
4564
|
|
|
4563
4565
|
/**
|
|
@@ -4641,7 +4643,7 @@ export async function handleRscResume(
|
|
|
4641
4643
|
// hole by construction: at build the params never settled, so anything
|
|
4642
4644
|
// reading them was postponed.
|
|
4643
4645
|
params?: Record<string, string>,
|
|
4644
|
-
): Promise<{ htmlStream: ReadableStream
|
|
4646
|
+
): Promise<{ htmlStream: ReadableStream }> {
|
|
4645
4647
|
await instrumented()
|
|
4646
4648
|
applyHost()
|
|
4647
4649
|
await runMiddleware(component, params ?? props)
|
|
@@ -4691,9 +4693,8 @@ export async function handleRscResume(
|
|
|
4691
4693
|
// client-rendered. Set before the stream ends, because that is when
|
|
4692
4694
|
// React reports it.
|
|
4693
4695
|
let replayFailed = false
|
|
4694
|
-
// The
|
|
4695
|
-
// the
|
|
4696
|
-
// that fetch - one render per visit.
|
|
4696
|
+
// The holes for the HTML, and the payload the browser hydrates from, from
|
|
4697
|
+
// the same render.
|
|
4697
4698
|
const [forHtml, rscPayload] = flight.tee()
|
|
4698
4699
|
const htmlStream = await ssr.handleSsrResume(
|
|
4699
4700
|
forHtml,
|
|
@@ -5650,6 +5651,43 @@ ${fallbackOrigin === false ? NO_FALLBACK_BODY : FALLBACK_BODY}}
|
|
|
5650
5651
|
* Without a not-found.tsx this is the string it always was.
|
|
5651
5652
|
*/
|
|
5652
5653
|
async function notFound(request: Request): Promise<Response> {
|
|
5654
|
+
// In the request's own scope. The host opens one around everything it
|
|
5655
|
+
// answers, and this is what is left once it has declined the url - so the
|
|
5656
|
+
// scope it had has closed. not-found.tsx, and the layouts it renders in,
|
|
5657
|
+
// read the request like any page: a cart badge, who is signed in. Outside
|
|
5658
|
+
// a scope that was "No request in scope", and the visitor got the bare
|
|
5659
|
+
// string below instead of the app.
|
|
5660
|
+
//
|
|
5661
|
+
// Only for a request that can show a page. A url no route owns, asked for
|
|
5662
|
+
// by an <img>, a <script>, a stylesheet - a browser's /favicon.ico, which
|
|
5663
|
+
// Lighthouse asks for on every run - cannot display not-found.tsx, and
|
|
5664
|
+
// rendering it ran every layout above it, cookie reads and queries
|
|
5665
|
+
// included, for an image request. Those get a plain 404 at once.
|
|
5666
|
+
if (!pageSaidNotFound(request) && !wantsPage(request)) {
|
|
5667
|
+
return new Response('Not found', { status: 404, headers: { 'Content-Type': 'text/plain; charset=utf-8' } })
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
return await withRequest(request, () => renderNotFound(request))
|
|
5671
|
+
}
|
|
5672
|
+
|
|
5673
|
+
/**
|
|
5674
|
+
* Whether the request can display a page: a navigation, an iframe, the
|
|
5675
|
+
* router's own payload request, or - from a client that does not say where
|
|
5676
|
+
* the answer goes - one that accepts HTML. Decided by what the request says
|
|
5677
|
+
* it wants, never by the url's extension: a route serving /sitemap.xml never
|
|
5678
|
+
* reaches this.
|
|
5679
|
+
*/
|
|
5680
|
+
function wantsPage(request: Request): boolean {
|
|
5681
|
+
if (request.headers.get('X-RSC')) return true
|
|
5682
|
+
|
|
5683
|
+
const dest = request.headers.get('Sec-Fetch-Dest')
|
|
5684
|
+
|
|
5685
|
+
if (dest) return dest === 'document' || dest === 'iframe' || dest === 'frame'
|
|
5686
|
+
|
|
5687
|
+
return (request.headers.get('Accept') ?? '').includes('text/html')
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
async function renderNotFound(request: Request): Promise<Response> {
|
|
5653
5691
|
${notFoundComponent
|
|
5654
5692
|
? `
|
|
5655
5693
|
// A navigation, not a document: answer with the not-found tree as a
|
|
@@ -5764,9 +5802,9 @@ export async function handleSsr(
|
|
|
5764
5802
|
// A crawler's request: answered once everything has rendered - see
|
|
5765
5803
|
// crawler.ts - so the host reads the page's final word on its status.
|
|
5766
5804
|
whole = false,
|
|
5767
|
-
// The
|
|
5768
|
-
//
|
|
5769
|
-
|
|
5805
|
+
// The host streams the payload into this document - see inlineFlight.ts.
|
|
5806
|
+
// Declared here so the runtime knows before its first chunk arrives.
|
|
5807
|
+
inline = false,
|
|
5770
5808
|
): Promise<ReadableStream> {
|
|
5771
5809
|
const root = withSearch(await createFromReadableStream(rscStream), search)
|
|
5772
5810
|
|
|
@@ -5777,9 +5815,8 @@ export async function handleSsr(
|
|
|
5777
5815
|
// seconds before the runtime hydrates is held for it rather than given
|
|
5778
5816
|
// to the browser as a document load. See earlyClicks.ts.
|
|
5779
5817
|
const bootstrapScriptContent = bootstrap
|
|
5780
|
-
? (
|
|
5781
|
-
EARLY +
|
|
5782
|
-
(await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
5818
|
+
? (inline ? 'self.__rsc_f=self.__rsc_f||[];' : '') +
|
|
5819
|
+
EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
5783
5820
|
: undefined
|
|
5784
5821
|
|
|
5785
5822
|
// Without an onError handler React rejects each abortable task on its own,
|