@rsc-kit/core 0.29.9 → 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/host.d.ts +5 -0
- package/dist/host.js +30 -6
- 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 +10 -4
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +5 -1
- 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/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 +63 -9
- package/dist/vite.js.map +1 -1
- package/package.json +1 -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/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,7 +3023,7 @@ 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
3029
|
import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -4537,7 +4537,7 @@ export async function handleRscHtmlStream(
|
|
|
4537
4537
|
nonce?: string,
|
|
4538
4538
|
pageKey = '',
|
|
4539
4539
|
bootstrap = true,
|
|
4540
|
-
): Promise<{ htmlStream: ReadableStream;
|
|
4540
|
+
): Promise<{ htmlStream: ReadableStream; rscPayload?: ReadableStream; clientChunks: unknown }> {
|
|
4541
4541
|
await instrumented()
|
|
4542
4542
|
applyHost()
|
|
4543
4543
|
await runMiddleware(component, props)
|
|
@@ -4545,11 +4545,21 @@ export async function handleRscHtmlStream(
|
|
|
4545
4545
|
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
|
|
4546
4546
|
{ onError: flightOnError },
|
|
4547
4547
|
)
|
|
4548
|
-
const [forHtml, forPayload] = flight.tee()
|
|
4549
|
-
const rscPayloadPromise = new Response(forPayload).text()
|
|
4550
4548
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4551
|
-
|
|
4552
|
-
|
|
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: {} }
|
|
4553
4563
|
}
|
|
4554
4564
|
|
|
4555
4565
|
/**
|
|
@@ -4683,8 +4693,11 @@ export async function handleRscResume(
|
|
|
4683
4693
|
// client-rendered. Set before the stream ends, because that is when
|
|
4684
4694
|
// React reports it.
|
|
4685
4695
|
let replayFailed = false
|
|
4696
|
+
// The holes for the HTML, and the payload the browser hydrates from, from
|
|
4697
|
+
// the same render.
|
|
4698
|
+
const [forHtml, rscPayload] = flight.tee()
|
|
4686
4699
|
const htmlStream = await ssr.handleSsrResume(
|
|
4687
|
-
|
|
4700
|
+
forHtml,
|
|
4688
4701
|
postponed,
|
|
4689
4702
|
nonce,
|
|
4690
4703
|
() => {
|
|
@@ -4693,7 +4706,7 @@ export async function handleRscResume(
|
|
|
4693
4706
|
requestSearch(),
|
|
4694
4707
|
)
|
|
4695
4708
|
|
|
4696
|
-
return { htmlStream, replayed: () => !replayFailed }
|
|
4709
|
+
return { htmlStream, replayed: () => !replayFailed, rscPayload }
|
|
4697
4710
|
}
|
|
4698
4711
|
|
|
4699
4712
|
// Server action (worker: rsc-action).
|
|
@@ -5638,6 +5651,43 @@ ${fallbackOrigin === false ? NO_FALLBACK_BODY : FALLBACK_BODY}}
|
|
|
5638
5651
|
* Without a not-found.tsx this is the string it always was.
|
|
5639
5652
|
*/
|
|
5640
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> {
|
|
5641
5691
|
${notFoundComponent
|
|
5642
5692
|
? `
|
|
5643
5693
|
// A navigation, not a document: answer with the not-found tree as a
|
|
@@ -5752,6 +5802,9 @@ export async function handleSsr(
|
|
|
5752
5802
|
// A crawler's request: answered once everything has rendered - see
|
|
5753
5803
|
// crawler.ts - so the host reads the page's final word on its status.
|
|
5754
5804
|
whole = false,
|
|
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,
|
|
5755
5808
|
): Promise<ReadableStream> {
|
|
5756
5809
|
const root = withSearch(await createFromReadableStream(rscStream), search)
|
|
5757
5810
|
|
|
@@ -5762,7 +5815,8 @@ export async function handleSsr(
|
|
|
5762
5815
|
// seconds before the runtime hydrates is held for it rather than given
|
|
5763
5816
|
// to the browser as a document load. See earlyClicks.ts.
|
|
5764
5817
|
const bootstrapScriptContent = bootstrap
|
|
5765
|
-
?
|
|
5818
|
+
? (inline ? 'self.__rsc_f=self.__rsc_f||[];' : '') +
|
|
5819
|
+
EARLY + (await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
5766
5820
|
: undefined
|
|
5767
5821
|
|
|
5768
5822
|
// Without an onError handler React rejects each abortable task on its own,
|