@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.
Files changed (42) hide show
  1. package/dist/action.d.ts +69 -9
  2. package/dist/action.js +78 -3
  3. package/dist/action.js.map +1 -1
  4. package/dist/conformance.d.ts +1 -0
  5. package/dist/conformance.js +24 -0
  6. package/dist/conformance.js.map +1 -1
  7. package/dist/headers.d.ts +0 -5
  8. package/dist/headers.js +0 -5
  9. package/dist/headers.js.map +1 -1
  10. package/dist/host.d.ts +3 -8
  11. package/dist/host.js +21 -73
  12. package/dist/host.js.map +1 -1
  13. package/dist/hostCalls.d.ts +7 -0
  14. package/dist/hostCalls.js +7 -1
  15. package/dist/hostCalls.js.map +1 -1
  16. package/dist/inlineFlight.d.ts +9 -0
  17. package/dist/inlineFlight.js +126 -0
  18. package/dist/inlineFlight.js.map +1 -0
  19. package/dist/js/Form.d.ts +9 -2
  20. package/dist/js/Form.js +11 -2
  21. package/dist/js/Form.js.map +1 -1
  22. package/dist/js/SegmentBoundary.js +4 -14
  23. package/dist/js/SegmentBoundary.js.map +1 -1
  24. package/dist/js/createViteRscApp.js +5 -8
  25. package/dist/js/createViteRscApp.js.map +1 -1
  26. package/dist/js/inlinePayload.d.ts +6 -0
  27. package/dist/js/inlinePayload.js +56 -0
  28. package/dist/js/inlinePayload.js.map +1 -0
  29. package/dist/js/pagePayload.d.ts +1 -1
  30. package/dist/js/pagePayload.js +2 -2
  31. package/dist/js/pagePayload.js.map +1 -1
  32. package/dist/js/useAction.d.ts +9 -6
  33. package/dist/js/useAction.js.map +1 -1
  34. package/dist/testHost.d.ts +6 -2
  35. package/dist/testHost.js +6 -2
  36. package/dist/testHost.js.map +1 -1
  37. package/dist/vite.js +61 -24
  38. package/dist/vite.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/bootPayloads.d.ts +0 -18
  41. package/dist/bootPayloads.js +0 -81
  42. 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"]}
@@ -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, headers?: Record<string, string>): Promise<Response>;
14
+ export declare function fetchPagePayload(url: string, fetchImpl?: typeof fetch): Promise<Response>;
@@ -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, headers = {}) {
16
+ export async function fetchPagePayload(url, fetchImpl = fetch) {
17
17
  let response;
18
18
  try {
19
- response = await fetchImpl(url, { headers: { "X-RSC": "1", ...headers } });
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,EAC/B,OAAO,GAA2B,EAAE;IAEpC,IAAI,QAAkB,CAAC;IAEvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,OAAO,EAAE,EAAE,CAAC,CAAC;IAC7E,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 headers: Record<string, string> = {},\n): Promise<Response> {\n let response: Response;\n\n try {\n response = await fetchImpl(url, { headers: { \"X-RSC\": \"1\", ...headers } });\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"]}
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"]}
@@ -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: ActionResult<DataOf<A>>) => void;
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: ActionResult<DataOf<A>>) => void;
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<ActionResult<DataOf<A>>>;
52
+ executeAsync: (...args: Parameters<A>) => Promise<ResultOf<A>>;
50
53
  isPending: boolean;
51
54
  status: UseActionStatus;
52
- result: ActionResult<DataOf<A>>;
55
+ result: ResultOf<A>;
53
56
  reset: () => void;
54
57
  };
55
58
  export default useAction;
@@ -1 +1 @@
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>>;\n\ntype DataOf<A extends AnyAction> = Awaited<ReturnType<A>> extends ActionResult<infer D> ? D : never;\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: ActionResult<DataOf<A>>) => void;\n /** After either, for state that tracks the call rather than its outcome. */\n onSettled?: (result: ActionResult<DataOf<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 = ActionResult<DataOf<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"]}
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"]}
@@ -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
- /** Refuse with a status - a 404 for a record this caller cannot see, a 429. */
106
- readonly refuse: (status: number, message?: string) => Reply;
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
- /** Refuse with a status - a 404 for a record this caller cannot see, a 429. */
44
- refuse: (status, message = 'Refused.') => reply({ error: message, refusalStatus: status }),
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. */
@@ -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: ReadableStream; bootToken?: string; clientChunks: unknown }> {
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
- const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler(), token)
4560
- return { htmlStream, rscPayload, bootToken: token, clientChunks: {} }
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; replayed: () => boolean; rscPayload: 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 same tree twice, as handleRscHtmlStream: the holes for the HTML, and
4695
- // the payload the browser comes back for. The host holds the second for
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 boot token: the document's render kept its payload under it, and
5768
- // the runtime redeems it instead of asking for a second render.
5769
- bootToken?: string,
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
- ? (bootToken ? 'self.__rsc_boot=' + JSON.stringify(bootToken) + ';' : '') +
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,