@rsc-kit/core 0.18.1 → 0.20.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 (77) hide show
  1. package/dist/action.d.ts +16 -2
  2. package/dist/action.js +21 -12
  3. package/dist/action.js.map +1 -1
  4. package/dist/apiPrerender.js +25 -7
  5. package/dist/apiPrerender.js.map +1 -1
  6. package/dist/clientPackages.d.ts +30 -0
  7. package/dist/clientPackages.js +234 -0
  8. package/dist/clientPackages.js.map +1 -0
  9. package/dist/compress.d.ts +12 -0
  10. package/dist/compress.js +134 -0
  11. package/dist/compress.js.map +1 -0
  12. package/dist/compressRuntime.d.ts +8 -0
  13. package/dist/compressRuntime.js +62 -0
  14. package/dist/compressRuntime.js.map +1 -0
  15. package/dist/files.d.ts +20 -0
  16. package/dist/files.js +38 -0
  17. package/dist/files.js.map +1 -1
  18. package/dist/formSubmit.d.ts +1 -0
  19. package/dist/formSubmit.js +14 -0
  20. package/dist/formSubmit.js.map +1 -0
  21. package/dist/host.d.ts +23 -0
  22. package/dist/host.js +175 -31
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +15 -0
  25. package/dist/hostCalls.js +75 -8
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/js/Form.d.ts +21 -4
  28. package/dist/js/Form.js +110 -83
  29. package/dist/js/Form.js.map +1 -1
  30. package/dist/js/Link.d.ts +5 -5
  31. package/dist/js/Link.js.map +1 -1
  32. package/dist/js/RedirectBoundary.js.map +1 -1
  33. package/dist/js/RouteErrorBoundary.d.ts +6 -2
  34. package/dist/js/RouteErrorBoundary.js +10 -0
  35. package/dist/js/RouteErrorBoundary.js.map +1 -1
  36. package/dist/js/SlotBoundary.d.ts +7 -1
  37. package/dist/js/SlotBoundary.js +9 -1
  38. package/dist/js/SlotBoundary.js.map +1 -1
  39. package/dist/js/createViteRscApp.d.ts +1 -0
  40. package/dist/js/createViteRscApp.js +54 -5
  41. package/dist/js/createViteRscApp.js.map +1 -1
  42. package/dist/js/errors.d.ts +3 -1
  43. package/dist/js/errors.js +26 -3
  44. package/dist/js/errors.js.map +1 -1
  45. package/dist/js/formEncoding.d.ts +72 -3
  46. package/dist/js/formEncoding.js +284 -20
  47. package/dist/js/formEncoding.js.map +1 -1
  48. package/dist/js/navigate.d.ts +6 -3
  49. package/dist/js/navigate.js +57 -7
  50. package/dist/js/navigate.js.map +1 -1
  51. package/dist/js/nuqs.js.map +1 -1
  52. package/dist/js/router.d.ts +3 -3
  53. package/dist/js/router.js.map +1 -1
  54. package/dist/js/updateStore.js +8 -2
  55. package/dist/js/updateStore.js.map +1 -1
  56. package/dist/manifest.d.ts +6 -0
  57. package/dist/manifest.js.map +1 -1
  58. package/dist/openapi.d.ts +74 -0
  59. package/dist/openapi.js +172 -0
  60. package/dist/openapi.js.map +1 -0
  61. package/dist/redirect.d.ts +4 -4
  62. package/dist/redirect.js.map +1 -1
  63. package/dist/request.d.ts +56 -2
  64. package/dist/request.js +68 -4
  65. package/dist/request.js.map +1 -1
  66. package/dist/routes.d.ts +17 -6
  67. package/dist/routes.js.map +1 -1
  68. package/dist/testing.d.ts +14 -0
  69. package/dist/testing.js +56 -2
  70. package/dist/testing.js.map +1 -1
  71. package/dist/useSsr.d.ts +11 -0
  72. package/dist/useSsr.js +15 -0
  73. package/dist/useSsr.js.map +1 -1
  74. package/dist/vite.d.ts +69 -1
  75. package/dist/vite.js +481 -51
  76. package/dist/vite.js.map +1 -1
  77. package/package.json +5 -1
@@ -1 +1 @@
1
- {"version":3,"file":"createViteRscApp.js","sourceRoot":"","sources":["../../src/js/createViteRscApp.ts"],"names":[],"mappings":"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","sourcesContent":["// Vite-engine client bootstrap. Uses @vitejs/plugin-rsc's browser runtime as\n// the Flight deserializer + action encoder, and drives the package's engine-agnostic\n// navigate.ts SPA engine (Link, prefetch, popstate) through it. This replaces\n// the bun engine's createRscApp + the hand-rolled webpack shim — the plugin\n// resolves client references itself.\nimport { SEARCH_PARAMS_FALLBACK } from \"./useSearchParams\";\nimport { recoverFromStaleAssets } from \"./staleAssets\";\nimport { showDevNotice } from \"./devNotice\";\nimport { caughtByLoading } from \"./fallbackReport\";\nimport { noteNavigation } from \"./segmentStore\";\nimport type { Href } from \"../routes.js\";\nimport { isSafeRedirect } from \"../safeUrl.js\";\nimport {\n createFromReadableStream,\n encodeReply,\n setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ActivityRoot } from \"./ActivityRouter\";\nimport { ServerRedirectError, throwForFailedAction } from \"./errors\";\nimport { fetchPagePayload } from \"./pagePayload\";\nimport { claimRead, setQueryCodec } from \"./queryClient\";\nimport { clearSegments, restoreSegments, setSegment } from \"./segmentStore\";\nimport type { ReactNode } from \"react\";\nimport {\n cancelPrefetch,\n isPrefetched,\n navigate,\n refresh,\n applyRevalidated,\n prefetch,\n retentionKey,\n setCallServer,\n setHeldLayouts,\n setStaticPayloads,\n setStaticRoutes,\n seedStaticChain,\n payloadUrl,\n setDeserializer,\n setInterceptManifest,\n setNavigateHandler,\n setRestoreHandler,\n setVersion,\n} from \"./navigate\";\n\nexport async function createViteRscApp(\n container: Document | Element = document,\n interceptEntries: { urlPattern: string; slot: string }[] = [],\n options: { staticPayloads?: string | null; routes?: unknown[] | null } = {},\n): Promise<void> {\n // An exported build has no server to negotiate with, so payloads live at\n // their own urls rather than behind a header on the page's url.\n setStaticPayloads(options.staticPayloads ?? null);\n // Only an exported build ships this: with no server to negotiate with, the\n // client works out for itself how much of a page it still holds.\n if (options.routes) setStaticRoutes(options.routes as never);\n // The router has to recognise an intercepted link before it asks the server,\n // so the patterns are baked into the generated browser entry. Without them\n // every intercepted route falls through to a full-page navigation.\n setInterceptManifest(interceptEntries);\n\n async function callServer(id: string, args: unknown[]): Promise<unknown> {\n // A read opened a slot before calling the reference, and this is the first\n // point at which the id exists on the client — React keeps it private and\n // hands it over only here. Claiming the slot enqueues the read for the\n // batched GET instead of posting it.\n const read = claimRead(id, args);\n\n if (read) return await read;\n\n const encoded = await encodeReply(args);\n\n // encodeReply returns FormData as soon as an argument contains a File.\n // Sending that as multipart would make PHP consume php://input while\n // parsing it, leaving the action with an empty body — so serialize it to\n // raw bytes under an opaque content-type and send the real one in\n // X-RSC-Content-Type for the worker to rebuild FormData from.\n let body: BodyInit;\n let realContentType: string;\n\n if (encoded instanceof FormData) {\n const serialized = new Response(encoded);\n body = await serialized.arrayBuffer();\n realContentType =\n serialized.headers.get(\"content-type\") ?? \"multipart/form-data\";\n } else {\n body = encoded as BodyInit;\n realContentType = \"text/plain;charset=UTF-8\";\n }\n\n const res = await fetch(\"/_rsc/action\", {\n method: \"POST\",\n headers: {\n \"X-RSC-Action\": id,\n // Where the action was invoked from. The host resolves it to the\n // components that render the page, so anything the action says it\n // invalidated can come back with the answer instead of being fetched\n // afterwards.\n \"X-RSC-Referer\": window.location.pathname + window.location.search,\n \"X-RSC-Content-Type\": realContentType,\n \"Content-Type\": \"application/octet-stream\",\n \"X-XSRF-TOKEN\": decodeURIComponent(\n document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? \"\",\n ),\n },\n body,\n });\n\n // A failed action does not answer with a Flight stream, and the decoder\n // cannot tell — it reports its own parse failure instead of what the\n // server said.\n try {\n await throwForFailedAction(res);\n } catch (err) {\n // A redirect is an instruction, not something for a form to display:\n // an expired session answers this way, and the destination is the login\n // page rather than a message about one.\n if (err instanceof ServerRedirectError) {\n // The destination came off a response header, so it is checked here\n // too — the engine refuses these at the source, but a proxy or a host\n // in front of it can write whatever it likes.\n if (isSafeRedirect(err.location)) window.location.href = err.location;\n }\n\n throw err;\n }\n\n const answer = await createFromReadableStream(res.body!, { callServer });\n\n return unwrapRevalidated(answer);\n }\n\n /**\n * Put anything the action re-rendered on screen, and hand back its result.\n *\n * The trees travel with the answer rather than being fetched afterwards, so\n * the caller sees only what its action returned and never knows the page\n * was updated around it.\n */\n function unwrapRevalidated(answer: unknown): unknown {\n if (\n answer === null ||\n typeof answer !== \"object\" ||\n !(\"__rscRevalidated\" in answer)\n ) {\n return answer;\n }\n\n const envelope = answer as {\n __rscRevalidated: Record<string, unknown>;\n result: unknown;\n };\n\n for (const [target, tree] of Object.entries(envelope.__rscRevalidated)) {\n applyRevalidated(target, tree);\n }\n\n return envelope.result;\n }\n\n setDeserializer(createFromReadableStream as never);\n // The query transport speaks Flight too, and reaches the runtime through here\n // rather than importing it: a second import of the browser runtime from the\n // client-component graph is a second client-reference registry, and the\n // symptom is components that render as undefined with nothing logged.\n setQueryCodec({\n deserialize: (stream) =>\n createFromReadableStream(stream, { callServer }) as Promise<unknown>,\n encode: (args) => encodeReply(args) as Promise<string | FormData>,\n // The way back when a read cannot ride in a url. Re-entering callServer is\n // safe: the slot was cleared the moment the reference returned, so this\n // takes the ordinary POST path rather than claiming a read again.\n asAction: (id, args) => callServer(id, args),\n });\n setCallServer(callServer);\n // The plugin's own \"use server\" client stubs route through its registered\n // server callback — register the same transport there too.\n setServerCallback(callServer as never);\n\n // Link / Form / router live in a separate build graph and reach the SPA\n // engine through these globals.\n (window as unknown as { __rsc_navigate: typeof navigate }).__rsc_navigate =\n navigate;\n (window as unknown as { __rsc_prefetch: typeof prefetch }).__rsc_prefetch =\n prefetch;\n (\n window as unknown as { __rsc_cancel_prefetch: typeof cancelPrefetch }\n ).__rsc_cancel_prefetch = cancelPrefetch;\n (\n window as unknown as { __rsc_is_prefetched: typeof isPrefetched }\n ).__rsc_is_prefetched = isPrefetched;\n (window as unknown as { __rsc_refresh: typeof refresh }).__rsc_refresh =\n refresh;\n\n // Hydrate from the RSC endpoint (same url + X-RSC, no version header).\n //\n // Everything from here to the decode is failure handling, because this is\n // the one request with nothing watching it. A page whose shell is already\n // rendered looks fine while this fails, and its fallbacks stay on screen\n // indefinitely with nothing reported anywhere.\n const res = await fetchPagePayload(payloadUrl(window.location.href));\n\n // Seed the SPA engine with the build this page was served from, so a\n // redeploy mid-session is caught on the next navigation. This matters most\n // behind a CDN, where the shell may be cached from an older build.\n const servedVersion = res.headers.get(\"X-RSC-Version\");\n\n if (servedVersion) {\n setVersion(servedVersion);\n }\n\n // The chain this page is built from, so the next navigation can say what is\n // already mounted and be sent only what changed.\n // A server says what this page is built from; a file server says nothing,\n // so an exported build works it out from the table it was given.\n const servedLayouts = res.headers.get(\"X-RSC-Layouts\");\n\n if (servedLayouts !== null) {\n setHeldLayouts(servedLayouts.split(\",\"));\n } else if (!seedStaticChain(window.location.href)) {\n setHeldLayouts([]);\n }\n const tree = await createFromReadableStream(res.body!, { callServer });\n\n // Retaining the previous page behind <Activity> needs a wrapper above the\n // page, and React will not hydrate a *document* container through one: the\n // root child of a document has to be <html>, and wrapping it hangs the\n // renderer outright (React 19.2.7). An app whose root layout owns <html>\n // therefore hydrates the tree directly and navigations replace it, as before.\n //\n // Apps that hydrate into an element get retention. Giving it to document-\n // rooted apps means SPA navigation returning only the changed segment rather\n // than a whole document, which is an engine change, not a client one.\n const retains = container !== document;\n\n const shell = retains\n ? createElement(ActivityRoot, {\n initialKey: retentionKey(window.location.href, null),\n initialTree: tree as ReactNode,\n })\n : (tree as ReactNode);\n\n const root = hydrateRoot(container, shell, {\n onRecoverableError(error: unknown, errorInfo: unknown) {\n // A client component whose chunk the server no longer has fails while\n // hydrating, and React reports that as recoverable: the page is loaded\n // again, with the current names.\n if (recoverFromStaleAssets(error)) return;\n\n // A PPR shell is served with its Suspense boundaries deliberately\n // unfinished — the build aborts the render once the static part is out.\n // React reports that as #419 and client-renders the boundary from the\n // Flight payload, which is the intended path, not a fault to report.\n const message = String((error as { message?: string })?.message ?? error);\n\n if (\n message.includes(\"419\") ||\n message.includes(\"did not finish this Suspense boundary\")\n ) {\n return;\n }\n\n // A view transition the browser cancelled because the tab was not\n // visible. React reports it here; the update still landed, and a tab\n // in the background is not a fault in the page.\n if (message.includes(\"Transition was aborted\")) return;\n // A page whose query string was read under a boundary carries the\n // fallback there; React reports the recovery on hydration. Under a\n // boundary the developer wrote, that is the designed path and nothing\n // is said. With nothing closer than a loading.tsx, the whole segment\n // showed the fallback until the query arrived - said here, in the\n // page and the console, for whoever is looking at either. Production\n // has no stack to tell the two apart; the build's note on the route is\n // the record there.\n if (\n (error as { digest?: string })?.digest === SEARCH_PARAMS_FALLBACK ||\n message.includes(\"useSearchParams()\")\n ) {\n const stack = (errorInfo as { componentStack?: string } | null)\n ?.componentStack;\n\n if (!caughtByLoading(stack)) return;\n\n const hint =\n \"useSearchParams() was read on the server with nothing closer than a loading.tsx, so the \" +\n \"whole segment showed that fallback until the query arrived. A <Suspense> around the \" +\n \"component that reads keeps the rest of the page painted.\";\n\n showDevNotice(hint, stack);\n console.error(new Error(hint), errorInfo);\n\n return;\n }\n\n console.error(error, errorInfo);\n },\n // A chunk the deploy no longer serves is not a fault in the page; the\n // document is loaded again, and the new names come with it. Anything\n // else is reported the way React would have.\n onUncaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n\n console.error(error, errorInfo);\n },\n onCaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n\n console.error(error, errorInfo);\n },\n });\n\n // Depth 0 is a whole document and replaces the root. Anything deeper is one\n // segment: handing it to the boundary at that depth leaves the layouts above\n // it mounted, which is the point of asking for a partial render at all.\n setNavigateHandler((tree: unknown, key: string, segmentDepth: number) => {\n const newTree = tree as ReactNode;\n\n // From here on the boundary animates; the seed commit before this did not.\n noteNavigation();\n\n if (segmentDepth > 0) {\n setSegment(segmentDepth, key, newTree);\n\n return;\n }\n\n clearSegments();\n root.render(newTree);\n });\n\n // Back and forward reveal a page the boundaries are still holding, with the\n // form you were filling in still filled in, and without asking the server.\n setRestoreHandler((key: string, maxAge?: number) =>\n restoreSegments(key, maxAge),\n );\n\n window.addEventListener(\"popstate\", () => {\n // restore: back and forward reveal the page you were on, with its state.\n // Wherever the browser just went; not a literal this app wrote.\n navigate(window.location.href as Href, { replace: true, restore: true });\n });\n\n history.replaceState(\n { rscUrl: window.location.href },\n \"\",\n window.location.href,\n );\n}\n"]}
1
+ {"version":3,"file":"createViteRscApp.js","sourceRoot":"","sources":["../../src/js/createViteRscApp.ts"],"names":[],"mappings":"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","sourcesContent":["// Vite-engine client bootstrap. Uses @vitejs/plugin-rsc's browser runtime as\n// the Flight deserializer + action encoder, and drives the package's engine-agnostic\n// navigate.ts SPA engine (Link, prefetch, popstate) through it. This replaces\n// the bun engine's createRscApp + the hand-rolled webpack shim — the plugin\n// resolves client references itself.\nimport { SEARCH_PARAMS_FALLBACK } from \"./useSearchParams\";\nimport { recoverFromStaleAssets } from \"./staleAssets\";\nimport { parseRedirectDigest } from \"../redirectDigest.js\";\nimport { showDevNotice } from \"./devNotice\";\nimport { caughtByLoading } from \"./fallbackReport\";\nimport { noteNavigation } from \"./segmentStore\";\nimport type { Route } from \"../routes.js\";\nimport { isSafeRedirect } from \"../safeUrl.js\";\nimport {\n createFromReadableStream,\n encodeReply,\n setServerCallback,\n} from \"@vitejs/plugin-rsc/browser\";\nimport { createElement } from \"react\";\nimport { hydrateRoot } from \"react-dom/client\";\nimport { ActivityRoot } from \"./ActivityRouter\";\nimport { ServerRedirectError, noteRedirected, throwForFailedAction } from \"./errors\";\nimport { fetchPagePayload } from \"./pagePayload\";\nimport { claimRead, setQueryCodec } from \"./queryClient\";\nimport { clearSegments, restoreSegments, setSegment } from \"./segmentStore\";\nimport type { ReactNode } from \"react\";\nimport {\n cancelPrefetch,\n isPrefetched,\n navigate,\n setApiRoutes,\n refresh,\n applyRevalidated,\n prefetch,\n retentionKey,\n setCallServer,\n setHeldLayouts,\n setStaticPayloads,\n setStaticRoutes,\n seedStaticChain,\n payloadUrl,\n setDeserializer,\n setInterceptManifest,\n setNavigateHandler,\n setRestoreHandler,\n setVersion,\n} from \"./navigate\";\n\n\n/**\n * A redirect that travelled as an error: the digest of the error itself, or\n * of the cause React wraps it in when it reports client-rendering a boundary\n * the server had already failed.\n */\nfunction isRedirect(error: unknown): boolean {\n const digest = (error as { digest?: unknown } | null)?.digest;\n const cause = (error as { cause?: { digest?: unknown } } | null)?.cause;\n\n return parseRedirectDigest(digest) !== null || parseRedirectDigest(cause?.digest) !== null;\n}\n\nexport async function createViteRscApp(\n container: Document | Element = document,\n interceptEntries: { urlPattern: string; slot: string }[] = [],\n options: { staticPayloads?: string | null; routes?: unknown[] | null; apiRoutes?: string[] } = {},\n): Promise<void> {\n // The urls a route.ts answers: a link to one is an anchor, never a\n // prefetch, never a payload fetch.\n if (options.apiRoutes) setApiRoutes(options.apiRoutes);\n // An exported build has no server to negotiate with, so payloads live at\n // their own urls rather than behind a header on the page's url.\n setStaticPayloads(options.staticPayloads ?? null);\n // Only an exported build ships this: with no server to negotiate with, the\n // client works out for itself how much of a page it still holds.\n if (options.routes) setStaticRoutes(options.routes as never);\n // The router has to recognise an intercepted link before it asks the server,\n // so the patterns are baked into the generated browser entry. Without them\n // every intercepted route falls through to a full-page navigation.\n setInterceptManifest(interceptEntries);\n\n async function callServer(id: string, args: unknown[]): Promise<unknown> {\n // A read opened a slot before calling the reference, and this is the first\n // point at which the id exists on the client — React keeps it private and\n // hands it over only here. Claiming the slot enqueues the read for the\n // batched GET instead of posting it.\n const read = claimRead(id, args);\n\n if (read) return await read;\n\n const encoded = await encodeReply(args);\n\n // encodeReply returns FormData as soon as an argument contains a File.\n // Sending that as multipart would make PHP consume php://input while\n // parsing it, leaving the action with an empty body — so serialize it to\n // raw bytes under an opaque content-type and send the real one in\n // X-RSC-Content-Type for the worker to rebuild FormData from.\n let body: BodyInit;\n let realContentType: string;\n\n if (encoded instanceof FormData) {\n const serialized = new Response(encoded);\n body = await serialized.arrayBuffer();\n realContentType =\n serialized.headers.get(\"content-type\") ?? \"multipart/form-data\";\n } else {\n body = encoded as BodyInit;\n realContentType = \"text/plain;charset=UTF-8\";\n }\n\n const res = await fetch(\"/_rsc/action\", {\n method: \"POST\",\n headers: {\n \"X-RSC-Action\": id,\n // Where the action was invoked from. The host resolves it to the\n // components that render the page, so anything the action says it\n // invalidated can come back with the answer instead of being fetched\n // afterwards.\n \"X-RSC-Referer\": window.location.pathname + window.location.search,\n \"X-RSC-Content-Type\": realContentType,\n \"Content-Type\": \"application/octet-stream\",\n \"X-XSRF-TOKEN\": decodeURIComponent(\n document.cookie.match(/XSRF-TOKEN=([^;]+)/)?.[1] ?? \"\",\n ),\n },\n body,\n });\n\n // A failed action does not answer with a Flight stream, and the decoder\n // cannot tell — it reports its own parse failure instead of what the\n // server said.\n try {\n await throwForFailedAction(res);\n } catch (err) {\n // A redirect is an instruction, not something for a form to display:\n // an expired session answers this way, and the destination is the login\n // page rather than a message about one.\n if (err instanceof ServerRedirectError) {\n // The destination came off a response header, so it is checked here\n // too — the engine refuses these at the source, but a proxy or a host\n // in front of it can write whatever it likes. A navigation, not a\n // document load: the layouts stay mounted and the history entry the\n // form was on is replaced, the way a redirect after a POST should\n // be - Back does not return to the submitted form.\n if (isSafeRedirect(err.location)) {\n void navigate(err.location as Route, { replace: true });\n }\n\n // Performed, so resolved - not thrown. The call's answer is \"you are\n // being taken somewhere\": an object saying so, the shape an action's\n // result already has, so a caller reading result.validationErrors\n // reads undefined rather than throwing. A caller that awaited the\n // action in a plain startTransition had no catch for a throw, the\n // rejection reached React, and the root unmounted to a white page\n // on every logout.\n noteRedirected(err.location);\n\n return { redirected: err.location };\n }\n\n throw err;\n }\n\n const answer = await createFromReadableStream(res.body!, { callServer });\n\n return unwrapRevalidated(answer);\n }\n\n /**\n * Put anything the action re-rendered on screen, and hand back its result.\n *\n * The trees travel with the answer rather than being fetched afterwards, so\n * the caller sees only what its action returned and never knows the page\n * was updated around it.\n */\n function unwrapRevalidated(answer: unknown): unknown {\n if (\n answer === null ||\n typeof answer !== \"object\" ||\n !(\"__rscRevalidated\" in answer)\n ) {\n return answer;\n }\n\n const envelope = answer as {\n __rscRevalidated: Record<string, unknown>;\n result: unknown;\n };\n\n for (const [target, tree] of Object.entries(envelope.__rscRevalidated)) {\n applyRevalidated(target, tree);\n }\n\n return envelope.result;\n }\n\n setDeserializer(createFromReadableStream as never);\n // The query transport speaks Flight too, and reaches the runtime through here\n // rather than importing it: a second import of the browser runtime from the\n // client-component graph is a second client-reference registry, and the\n // symptom is components that render as undefined with nothing logged.\n setQueryCodec({\n deserialize: (stream) =>\n createFromReadableStream(stream, { callServer }) as Promise<unknown>,\n encode: (args) => encodeReply(args) as Promise<string | FormData>,\n // The way back when a read cannot ride in a url. Re-entering callServer is\n // safe: the slot was cleared the moment the reference returned, so this\n // takes the ordinary POST path rather than claiming a read again.\n asAction: (id, args) => callServer(id, args),\n });\n setCallServer(callServer);\n // The plugin's own \"use server\" client stubs route through its registered\n // server callback — register the same transport there too.\n setServerCallback(callServer as never);\n\n // Link / Form / router live in a separate build graph and reach the SPA\n // engine through these globals.\n (window as unknown as { __rsc_navigate: typeof navigate }).__rsc_navigate =\n navigate;\n (window as unknown as { __rsc_prefetch: typeof prefetch }).__rsc_prefetch =\n prefetch;\n (\n window as unknown as { __rsc_cancel_prefetch: typeof cancelPrefetch }\n ).__rsc_cancel_prefetch = cancelPrefetch;\n (\n window as unknown as { __rsc_is_prefetched: typeof isPrefetched }\n ).__rsc_is_prefetched = isPrefetched;\n (window as unknown as { __rsc_refresh: typeof refresh }).__rsc_refresh =\n refresh;\n\n // Hydrate from the RSC endpoint (same url + X-RSC, no version header).\n //\n // Everything from here to the decode is failure handling, because this is\n // the one request with nothing watching it. A page whose shell is already\n // rendered looks fine while this fails, and its fallbacks stay on screen\n // indefinitely with nothing reported anywhere.\n const res = await fetchPagePayload(payloadUrl(window.location.href));\n\n // Seed the SPA engine with the build this page was served from, so a\n // redeploy mid-session is caught on the next navigation. This matters most\n // behind a CDN, where the shell may be cached from an older build.\n const servedVersion = res.headers.get(\"X-RSC-Version\");\n\n if (servedVersion) {\n setVersion(servedVersion);\n }\n\n // The chain this page is built from, so the next navigation can say what is\n // already mounted and be sent only what changed.\n // A server says what this page is built from; a file server says nothing,\n // so an exported build works it out from the table it was given.\n const servedLayouts = res.headers.get(\"X-RSC-Layouts\");\n\n if (servedLayouts !== null) {\n setHeldLayouts(servedLayouts.split(\",\"));\n } else if (!seedStaticChain(window.location.href)) {\n setHeldLayouts([]);\n }\n const tree = await createFromReadableStream(res.body!, { callServer });\n\n // Retaining the previous page behind <Activity> needs a wrapper above the\n // page, and React will not hydrate a *document* container through one: the\n // root child of a document has to be <html>, and wrapping it hangs the\n // renderer outright (React 19.2.7). An app whose root layout owns <html>\n // therefore hydrates the tree directly and navigations replace it, as before.\n //\n // Apps that hydrate into an element get retention. Giving it to document-\n // rooted apps means SPA navigation returning only the changed segment rather\n // than a whole document, which is an engine change, not a client one.\n const retains = container !== document;\n\n const shell = retains\n ? createElement(ActivityRoot, {\n initialKey: retentionKey(window.location.href, null),\n initialTree: tree as ReactNode,\n })\n : (tree as ReactNode);\n\n const root = hydrateRoot(container, shell, {\n onRecoverableError(error: unknown, errorInfo: unknown) {\n // A client component whose chunk the server no longer has fails while\n // hydrating, and React reports that as recoverable: the page is loaded\n // again, with the current names.\n if (recoverFromStaleAssets(error)) return;\n\n // A redirect decided inside a boundary: the server's row carried it as\n // the digest, React client-renders the boundary and reports that it\n // did, and the RedirectBoundary is already performing it. Nothing to\n // read in the console about a page doing what it was told.\n if (isRedirect(error)) return;\n\n // A PPR shell is served with its Suspense boundaries deliberately\n // unfinished — the build aborts the render once the static part is out.\n // React reports that as #419 and client-renders the boundary from the\n // Flight payload, which is the intended path, not a fault to report.\n const message = String((error as { message?: string })?.message ?? error);\n\n if (\n message.includes(\"419\") ||\n message.includes(\"did not finish this Suspense boundary\")\n ) {\n return;\n }\n\n // A view transition the browser cancelled because the tab was not\n // visible. React reports it here; the update still landed, and a tab\n // in the background is not a fault in the page.\n if (message.includes(\"Transition was aborted\")) return;\n\n // A navigation another one overtook: its request was aborted, and a\n // row of its payload that a component was still reading rejects with\n // the abort. React recovers by rendering the root again synchronously\n // and reports the recovery here as #520, with the abort as the cause.\n // The page it recovered to is the one the second navigation asked for;\n // the abort was the design, not a fault.\n const cause = (error as { cause?: { name?: string; message?: string } } | null)?.cause;\n\n if (\n cause?.name === \"AbortError\" ||\n /\\baborted\\b/i.test(cause?.message ?? \"\") ||\n /\\baborted\\b/i.test(message)\n ) {\n return;\n }\n // A page whose query string was read under a boundary carries the\n // fallback there; React reports the recovery on hydration. Under a\n // boundary the developer wrote, that is the designed path and nothing\n // is said. With nothing closer than a loading.tsx, the whole segment\n // showed the fallback until the query arrived - said here, in the\n // page and the console, for whoever is looking at either. Production\n // has no stack to tell the two apart; the build's note on the route is\n // the record there.\n if (\n (error as { digest?: string })?.digest === SEARCH_PARAMS_FALLBACK ||\n message.includes(\"useSearchParams()\")\n ) {\n const stack = (errorInfo as { componentStack?: string } | null)\n ?.componentStack;\n\n if (!caughtByLoading(stack)) return;\n\n const hint =\n \"useSearchParams() was read on the server with nothing closer than a loading.tsx, so the \" +\n \"whole segment showed that fallback until the query arrived. A <Suspense> around the \" +\n \"component that reads keeps the rest of the page painted.\";\n\n showDevNotice(hint, stack);\n console.error(new Error(hint), errorInfo);\n\n return;\n }\n\n console.error(error, errorInfo);\n },\n // A chunk the deploy no longer serves is not a fault in the page; the\n // document is loaded again, and the new names come with it. Anything\n // else is reported the way React would have.\n onUncaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n\n console.error(error, errorInfo);\n },\n onCaughtError(error: unknown, errorInfo: unknown) {\n if (recoverFromStaleAssets(error)) return;\n // Caught by the RedirectBoundary, which is performing it.\n if (isRedirect(error)) return;\n\n console.error(error, errorInfo);\n },\n });\n\n // Depth 0 is a whole document and replaces the root. Anything deeper is one\n // segment: handing it to the boundary at that depth leaves the layouts above\n // it mounted, which is the point of asking for a partial render at all.\n setNavigateHandler((tree: unknown, key: string, segmentDepth: number) => {\n const newTree = tree as ReactNode;\n\n // From here on the boundary animates; the seed commit before this did not.\n noteNavigation();\n\n if (segmentDepth > 0) {\n setSegment(segmentDepth, key, newTree);\n\n return;\n }\n\n clearSegments();\n root.render(newTree);\n });\n\n // Back and forward reveal a page the boundaries are still holding, with the\n // form you were filling in still filled in, and without asking the server.\n setRestoreHandler((key: string, maxAge?: number) =>\n restoreSegments(key, maxAge),\n );\n\n window.addEventListener(\"popstate\", () => {\n // restore: back and forward reveal the page you were on, with its state.\n // Wherever the browser just went; not a literal this app wrote.\n navigate(window.location.href as Route, { replace: true, restore: true });\n });\n\n history.replaceState(\n { rscUrl: window.location.href },\n \"\",\n window.location.href,\n );\n}\n"]}
@@ -2,7 +2,9 @@ export declare class ServerValidationError extends Error {
2
2
  readonly errors: Record<string, string[]>;
3
3
  constructor(message: string, errors: Record<string, string[]>);
4
4
  }
5
- /** An action answered with a location instead of a result. */
5
+ export declare function noteRedirected(location: string): void;
6
+ /** The redirect the action just performed, if it did - read once. */
7
+ export declare function redirectedTo(): string | null;
6
8
  export declare class ServerRedirectError extends Error {
7
9
  readonly location: string;
8
10
  constructor(location: string);
package/dist/js/errors.js CHANGED
@@ -7,6 +7,25 @@ export class ServerValidationError extends Error {
7
7
  }
8
8
  }
9
9
  /** An action answered with a location instead of a result. */
10
+ /**
11
+ * The redirect the last action answered with, for the one caller that asks.
12
+ *
13
+ * An action that redirects resolves - the navigation is already under way
14
+ * and the caller has nothing to do - which leaves a form that wants to know
15
+ * whether to say "saved" with no way to tell a redirect from a void answer.
16
+ * callServer notes the destination here; <Form> reads and clears it right
17
+ * after its await. Nothing else needs to.
18
+ */
19
+ let lastRedirect = null;
20
+ export function noteRedirected(location) {
21
+ lastRedirect = location;
22
+ }
23
+ /** The redirect the action just performed, if it did - read once. */
24
+ export function redirectedTo() {
25
+ const location = lastRedirect;
26
+ lastRedirect = null;
27
+ return location;
28
+ }
10
29
  export class ServerRedirectError extends Error {
11
30
  location;
12
31
  constructor(location) {
@@ -51,13 +70,17 @@ export class ServerSessionExpiredError extends Error {
51
70
  * Returns without throwing when the response is a stream to be decoded.
52
71
  */
53
72
  export async function throwForFailedAction(response) {
54
- if (response.ok)
55
- return;
56
- // Auth and explicit redirects travel as a header, whatever the status.
73
+ // Before the status: a redirect the action asked for is a 204 with the
74
+ // destination in this header - an ok answer with no Flight in it - and
75
+ // an expired session's is a 401. Read first, the header decides for both.
76
+ // Read after `ok`, the 204 fell through to the Flight decoder with an
77
+ // empty body, and the form waited forever.
57
78
  const location = response.headers.get("X-RSC-Redirect");
58
79
  if (location !== null && location !== "") {
59
80
  throw new ServerRedirectError(location);
60
81
  }
82
+ if (response.ok)
83
+ return;
61
84
  if (response.status === 422) {
62
85
  const payload = (await response.json().catch(() => null));
63
86
  throw new ServerValidationError(payload?.message ?? "Validation failed", payload?.errors ?? {});
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"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","sourcesContent":["export class ServerValidationError extends Error {\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n this.errors = errors;\n }\n}\n\n/** An action answered with a location instead of a result. */\nexport class ServerRedirectError extends Error {\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n }\n}\n\nexport class ServerDumpError extends Error {\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n if (response.ok) return;\n\n // Auth and explicit redirects travel as a header, whatever the status.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/js/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9B,MAAM,CAA2B;IAEjD,YAAY,OAAe,EAAE,MAAgC;QAC3D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,8DAA8D;AAC9D;;;;;;;;GAQG;AACH,IAAI,YAAY,GAAkB,IAAI,CAAC;AAEvC,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC7C,YAAY,GAAG,QAAQ,CAAC;AAC1B,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,YAAY,CAAC;IAE9B,YAAY,GAAG,IAAI,CAAC;IAEpB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5B,QAAQ,CAAS;IAEjC,YAAY,QAAgB;QAC1B,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,YAAY,OAAO,GAAW,kBAAkB;QAC9C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACjD,YAAY,OAAO,GAAW,8BAA8B;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC;QACE,KAAK,CAAC,kCAAkC,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,YAAY,OAAO,GAAW,oDAAoD;QAChF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAkB;IAC3D,uEAAuE;IACvE,uEAAuE;IACvE,0EAA0E;IAC1E,sEAAsE;IACtE,2CAA2C;IAC3C,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAExD,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;QACzC,MAAM,IAAI,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,QAAQ,CAAC,EAAE;QAAE,OAAO;IAExB,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAEhD,CAAC;QAET,MAAM,IAAI,qBAAqB,CAC7B,OAAO,EAAE,OAAO,IAAI,mBAAmB,EACvC,OAAO,EAAE,MAAM,IAAI,EAAE,CACtB,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,6EAA6E;YAC3E,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6BAA6B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAkB;IACtD,IAAI,QAAQ,CAAC,EAAE;QAAE,OAAO;IAExB,MAAM,IAAI,KAAK,CAAC,mCAAmC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AACxE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa,EAAE,KAAc;IAC/D,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,aAAa,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC;AAC7C,CAAC","sourcesContent":["export class ServerValidationError extends Error {\n public readonly errors: Record<string, string[]>;\n\n constructor(message: string, errors: Record<string, string[]>) {\n super(message);\n this.name = \"ServerValidationError\";\n this.errors = errors;\n }\n}\n\n/** An action answered with a location instead of a result. */\n/**\n * The redirect the last action answered with, for the one caller that asks.\n *\n * An action that redirects resolves - the navigation is already under way\n * and the caller has nothing to do - which leaves a form that wants to know\n * whether to say \"saved\" with no way to tell a redirect from a void answer.\n * callServer notes the destination here; <Form> reads and clears it right\n * after its await. Nothing else needs to.\n */\nlet lastRedirect: string | null = null;\n\nexport function noteRedirected(location: string): void {\n lastRedirect = location;\n}\n\n/** The redirect the action just performed, if it did - read once. */\nexport function redirectedTo(): string | null {\n const location = lastRedirect;\n\n lastRedirect = null;\n\n return location;\n}\n\nexport class ServerRedirectError extends Error {\n public readonly location: string;\n\n constructor(location: string) {\n super(`Server action redirected to ${location}`);\n this.name = \"ServerRedirectError\";\n this.location = location;\n }\n}\n\nexport class ServerAuthenticationError extends Error {\n constructor(message: string = \"Unauthenticated.\") {\n super(message);\n this.name = \"ServerAuthenticationError\";\n }\n}\n\nexport class ServerAuthorizationError extends Error {\n constructor(message: string = \"This action is unauthorized.\") {\n super(message);\n this.name = \"ServerAuthorizationError\";\n }\n}\n\nexport class ServerDumpError extends Error {\n constructor() {\n super(\"Server returned a dump response.\");\n this.name = \"ServerDumpError\";\n }\n}\n\nexport class ServerSessionExpiredError extends Error {\n constructor(message: string = \"Your session has expired. Please refresh the page.\") {\n super(message);\n this.name = \"ServerSessionExpiredError\";\n }\n}\n\n/**\n * Turn a failed server-action response into the error it describes.\n *\n * A server action that does not succeed answers with JSON or a redirect\n * header rather than a Flight stream. Passing one of those to the Flight\n * decoder does not produce the server's message — it produces an internal\n * parser failure (\"enqueueModel is not a function\") or a truncated read\n * (\"Connection closed.\"), which is what reached onError before this existed.\n *\n * Returns without throwing when the response is a stream to be decoded.\n */\nexport async function throwForFailedAction(response: Response): Promise<void> {\n // Before the status: a redirect the action asked for is a 204 with the\n // destination in this header - an ok answer with no Flight in it - and\n // an expired session's is a 401. Read first, the header decides for both.\n // Read after `ok`, the 204 fell through to the Flight decoder with an\n // empty body, and the form waited forever.\n const location = response.headers.get(\"X-RSC-Redirect\");\n\n if (location !== null && location !== \"\") {\n throw new ServerRedirectError(location);\n }\n\n if (response.ok) return;\n\n if (response.status === 422) {\n const payload = (await response.json().catch(() => null)) as\n | { message?: string; errors?: Record<string, string[]> }\n | null;\n\n throw new ServerValidationError(\n payload?.message ?? \"Validation failed\",\n payload?.errors ?? {},\n );\n }\n\n if (response.status === 413) {\n throw new Error(\n \"Server action refused: the request body is larger than the server accepts. \" +\n \"A file upload is the usual cause; the limit is the host's maxActionBody.\",\n );\n }\n\n throw new Error(`Server action failed with ${response.status}`);\n}\n\n/**\n * Reject a payload response that is not one.\n *\n * The page a PPR route serves is a static shell: real HTML, status 200, with\n * its Suspense fallbacks showing. Everything below them arrives in a second\n * request. If that request fails there is nothing on screen to say so — the\n * skeletons simply stay, for ever — and handing the failure body to the Flight\n * decoder reports the decoder's confusion rather than the status.\n */\nexport function throwForFailedPayload(response: Response): void {\n if (response.ok) return;\n\n throw new Error(`RSC payload request failed with ${response.status}`);\n}\n\n/**\n * Announce a failure that nothing else will.\n *\n * Dispatched as well as logged: an app that wants to replace a stuck skeleton\n * with something honest has no other way to find out.\n */\nexport function reportClientFailure(scope: string, error: unknown): void {\n if (typeof window !== \"undefined\") {\n window.dispatchEvent(new CustomEvent(\"rsc-client-error\", { detail: { scope, error } }));\n }\n\n console.error(`[rsc-kit] ${scope}`, error);\n}\n"]}
@@ -1,8 +1,77 @@
1
1
  /**
2
2
  * Serialize form state into FormData for a server action.
3
3
  *
4
- * The contract between a form and the action it calls — booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
5
- * `key[]`, Files pass through untouched for native uploads, and null/undefined
6
- * are dropped rather than sent as the string "null".
4
+ * The contract between a form and the action it calls: booleans become "1"/"0"
5
+ * so PHP sees something truthy, Files pass through untouched for native
6
+ * uploads, null/undefined are dropped rather than sent as the string "null",
7
+ * and a nested value nests - `{ items: [{ name }] }` is `items[0][name]`,
8
+ * `{ address: { city } }` is `address[city]`, a list of scalars repeats under
9
+ * `key[]`. Every one of those decodes back to what was given.
7
10
  */
8
11
  export declare function buildFormData(data: Record<string, unknown>): FormData;
12
+ /**
13
+ * A FormData as the object a schema expects.
14
+ *
15
+ * Four things beyond copying entries across, and each of them was a bug or a
16
+ * gap someone would meet on their first non-trivial form:
17
+ *
18
+ * A repeated name is an array. Three checkboxes sharing a name, a multiple
19
+ * select, a list of tags - this used to keep the LAST one and drop the rest
20
+ * silently, so a schema validated an object the person had not submitted.
21
+ *
22
+ * A name ending in `[]` is always an array, even with one value selected.
23
+ * Otherwise a list of checkboxes is a string when one is ticked and an array
24
+ * when two are, and no schema can describe both. It is also what `useForm`
25
+ * writes when it serialises an array, so the two round-trip.
26
+ *
27
+ * Nested names nest. `address.city` and `items[0].name` build the object they
28
+ * describe, which is the shape the schema was written against - and the shape
29
+ * whose validation errors come back keyed the same way, because Standard
30
+ * Schema issue paths are joined with dots too.
31
+ *
32
+ * Files are kept. They were dropped for being non-strings, which meant a schema
33
+ * checking an upload was handed undefined and refused a file that was there.
34
+ */
35
+ export declare function formDataToObject<T extends Record<string, unknown> = Record<string, unknown>>(formData: FormData): T;
36
+ type JsonSchema = {
37
+ type?: string | string[];
38
+ properties?: Record<string, JsonSchema>;
39
+ items?: JsonSchema | JsonSchema[];
40
+ required?: string[];
41
+ anyOf?: JsonSchema[];
42
+ oneOf?: JsonSchema[];
43
+ const?: unknown;
44
+ enum?: unknown[];
45
+ };
46
+ /**
47
+ * A form's values as the types its schema means.
48
+ *
49
+ * Everything a form posts is a string or a file, and a control that is off
50
+ * is not there at all - so a schema written for the shape it wants, `boolean`
51
+ * or `number`, refused a form that was perfectly filled in, and the guide
52
+ * asked for `z.enum(['on', '1']).optional().transform(...)` per checkbox. The
53
+ * schema knows what it means; this reads the form the way the schema does:
54
+ *
55
+ * - a `boolean` that is absent is `false` (an unchecked box posts nothing);
56
+ * `"on"`, `"1"`, `"true"` are true, `"0"`, `"off"`, `"false"` are false
57
+ * - a `number` or `integer` given as a numeric string is the number; an
58
+ * empty string for one that is not required is absent
59
+ * - an `array` given one value is that value in a list, and one given
60
+ * nothing - no checkbox ticked - is the empty list
61
+ * - an object nests, a list of objects nests per item, and a union takes the
62
+ * branch its discriminator names
63
+ *
64
+ * Nothing else is touched: a string stays the string it was, a file stays a
65
+ * file, and a value the schema does not describe is passed through as posted.
66
+ */
67
+ export declare function coerceToSchema(value: unknown, schema: JsonSchema | null | undefined): unknown;
68
+ /**
69
+ * A FormData as the object a schema expects, typed the way the schema means.
70
+ *
71
+ * What `<Form>` validates and what an action decodes: the same call, so a
72
+ * form that passes in the browser passes on the server. Without a schema, or
73
+ * with one that cannot describe itself, the values are the strings the form
74
+ * posted.
75
+ */
76
+ export declare function decodeFormData<T extends Record<string, unknown> = Record<string, unknown>>(formData: FormData, schema?: unknown): T;
77
+ export {};
@@ -1,37 +1,301 @@
1
- // Turning an object back into FormData.
1
+ // Turning an object into FormData, and FormData back into an object.
2
2
  //
3
3
  // The one place the encoding decisions live, so a value sent through `<Form>`'s
4
- // `transform` is encoded exactly as one sent any other way. It used to exist
5
- // twice — here and inline in Form.tsx — which is two places for a boolean to
6
- // start meaning something different.
4
+ // `transform` is encoded exactly as one sent any other way, and - the part
5
+ // that mattered - the object an ACTION decodes is the same object the FORM
6
+ // validated. The decoder lived in Form.tsx and understood nested names;
7
+ // the action had a flat one of its own. A form with `items[0].name` passed
8
+ // validation in the browser and failed the same schema on the server, and
9
+ // nothing said why. Both sides call this now.
7
10
  /**
8
11
  * Serialize form state into FormData for a server action.
9
12
  *
10
- * The contract between a form and the action it calls — booleans become "1"/"0" so PHP sees something truthy, arrays repeat under
11
- * `key[]`, Files pass through untouched for native uploads, and null/undefined
12
- * are dropped rather than sent as the string "null".
13
+ * The contract between a form and the action it calls: booleans become "1"/"0"
14
+ * so PHP sees something truthy, Files pass through untouched for native
15
+ * uploads, null/undefined are dropped rather than sent as the string "null",
16
+ * and a nested value nests - `{ items: [{ name }] }` is `items[0][name]`,
17
+ * `{ address: { city } }` is `address[city]`, a list of scalars repeats under
18
+ * `key[]`. Every one of those decodes back to what was given.
13
19
  */
14
20
  export function buildFormData(data) {
15
21
  const formData = new FormData();
16
- for (const [key, val] of Object.entries(data)) {
17
- if (val === null || val === undefined) {
22
+ for (const [key, val] of Object.entries(data))
23
+ appendValue(formData, key, val);
24
+ return formData;
25
+ }
26
+ function appendValue(formData, key, val) {
27
+ if (val === null || val === undefined)
28
+ return;
29
+ if (val instanceof File || val instanceof Blob) {
30
+ formData.append(key, val);
31
+ }
32
+ else if (typeof val === "boolean") {
33
+ formData.append(key, val ? "1" : "0");
34
+ }
35
+ else if (Array.isArray(val)) {
36
+ // Scalars repeat under key[]; objects and nested lists take an index, so
37
+ // each element's fields stay together on the way back.
38
+ val.forEach((item, i) => {
39
+ if (item !== null && typeof item === "object" && !(item instanceof File) && !(item instanceof Blob)) {
40
+ appendValue(formData, `${key}[${i}]`, item);
41
+ }
42
+ else {
43
+ appendValue(formData, `${key}[]`, item);
44
+ }
45
+ });
46
+ }
47
+ else if (typeof val === "object" && !(val instanceof Date)) {
48
+ for (const [prop, inner] of Object.entries(val)) {
49
+ appendValue(formData, `${key}[${prop}]`, inner);
50
+ }
51
+ }
52
+ else if (val instanceof Date) {
53
+ formData.append(key, val.toISOString());
54
+ }
55
+ else {
56
+ formData.append(key, String(val));
57
+ }
58
+ }
59
+ /**
60
+ * The pieces of a field name: `items[0].name` is items, 0, name.
61
+ *
62
+ * Both spellings, because both are in use and a form should not care which one
63
+ * a person reached for: `items[0].name` and `items[0][name]` are the same
64
+ * field. A trailing `[]` is a piece of its own - see below.
65
+ */
66
+ function pathOf(name) {
67
+ return name
68
+ .replace(/\[(\w*)\]/g, ".$1")
69
+ .split(".")
70
+ .filter((piece, index, all) => piece !== "" || index === all.length - 1);
71
+ }
72
+ /** Whether a piece names an array index rather than a property. */
73
+ const isIndex = (piece) => /^\d+$/.test(piece);
74
+ /**
75
+ * Put one value at one path, making the containers it passes through.
76
+ *
77
+ * Whether a container is an array or an object is decided by the NEXT piece, so
78
+ * `items[0].name` makes an array holding an object without being told which is
79
+ * which.
80
+ */
81
+ function place(root, path, value) {
82
+ let node = root;
83
+ for (let i = 0; i < path.length - 1; i++) {
84
+ const key = path[i];
85
+ const container = node;
86
+ if (container[key] === undefined || typeof container[key] !== "object") {
87
+ // An index makes an array, and so does the empty piece a trailing `[]`
88
+ // leaves - `tags[]` has to reach an array to be pushed into, and building
89
+ // an object there is how this first went wrong.
90
+ const next = path[i + 1];
91
+ container[key] = isIndex(next) || next === "" ? [] : {};
92
+ }
93
+ node = container[key];
94
+ }
95
+ const last = path[path.length - 1];
96
+ // The empty piece a trailing `[]` leaves: push rather than assign, so
97
+ // `tags[]` twice is two entries rather than one overwriting the other.
98
+ if (last === "")
99
+ node.push(value);
100
+ else
101
+ node[last] = value;
102
+ }
103
+ /**
104
+ * A FormData as the object a schema expects.
105
+ *
106
+ * Four things beyond copying entries across, and each of them was a bug or a
107
+ * gap someone would meet on their first non-trivial form:
108
+ *
109
+ * A repeated name is an array. Three checkboxes sharing a name, a multiple
110
+ * select, a list of tags - this used to keep the LAST one and drop the rest
111
+ * silently, so a schema validated an object the person had not submitted.
112
+ *
113
+ * A name ending in `[]` is always an array, even with one value selected.
114
+ * Otherwise a list of checkboxes is a string when one is ticked and an array
115
+ * when two are, and no schema can describe both. It is also what `useForm`
116
+ * writes when it serialises an array, so the two round-trip.
117
+ *
118
+ * Nested names nest. `address.city` and `items[0].name` build the object they
119
+ * describe, which is the shape the schema was written against - and the shape
120
+ * whose validation errors come back keyed the same way, because Standard
121
+ * Schema issue paths are joined with dots too.
122
+ *
123
+ * Files are kept. They were dropped for being non-strings, which meant a schema
124
+ * checking an upload was handed undefined and refused a file that was there.
125
+ */
126
+ export function formDataToObject(formData) {
127
+ const obj = {};
128
+ for (const name of new Set(formData.keys())) {
129
+ const all = formData.getAll(name);
130
+ const path = pathOf(name);
131
+ // A plain name used more than once is the array case, and it has no
132
+ // brackets to say so - `tags` twice is `['a', 'b']`.
133
+ if (path.length === 1 && path[0] !== "" && all.length > 1) {
134
+ obj[path[0]] = all;
18
135
  continue;
19
136
  }
20
- if (val instanceof File) {
21
- formData.append(key, val);
137
+ for (const value of all)
138
+ place(obj, path, value);
139
+ }
140
+ return obj;
141
+ }
142
+ /**
143
+ * What a schema is asked for. A leaf JSON Schema cannot say - a Date, a
144
+ * Map, a custom check - is `{}` rather than a refusal of the whole schema:
145
+ * Zod reads `unrepresentable` from here, and a form whose schema has one
146
+ * `z.date()` beside twenty fields still reads the twenty the way they mean.
147
+ * A library that does not know the option ignores it.
148
+ */
149
+ const JSON_SCHEMA_OPTIONS = { target: "draft-2020-12", libraryOptions: { unrepresentable: "any" } };
150
+ const jsonSchemas = new WeakMap();
151
+ /**
152
+ * The JSON Schema a Standard Schema describes itself with, if it can.
153
+ *
154
+ * Zod 4 and ArkType can; Valibot needs its separate converter and answers
155
+ * nothing here, in which case the form's values are handed to the schema as
156
+ * posted and the schema has to read strings.
157
+ */
158
+ function jsonSchemaOf(schema) {
159
+ if (schema === null || typeof schema !== "object")
160
+ return null;
161
+ const cached = jsonSchemas.get(schema);
162
+ if (cached !== undefined)
163
+ return cached;
164
+ let json = null;
165
+ const produce = schema["~standard"]?.jsonSchema?.input;
166
+ try {
167
+ if (typeof produce === "function") {
168
+ json = produce(JSON_SCHEMA_OPTIONS);
169
+ }
170
+ }
171
+ catch (error) {
172
+ json = null;
173
+ // A schema that has the method and still refused: every field of this
174
+ // form now arrives as the string it was posted, and a z.boolean() in it
175
+ // fails on "on" - which looks like the form's fault. Said once, in
176
+ // development, with the library's own reason.
177
+ if (typeof produce === "function" && import.meta.env?.DEV) {
178
+ console.warn("[rsc-kit] this form's schema cannot describe itself as JSON Schema, so its values are " +
179
+ "validated as the strings the form posted: " +
180
+ (error instanceof Error ? error.message : String(error)));
22
181
  }
23
- else if (typeof val === "boolean") {
24
- formData.append(key, val ? "1" : "0");
182
+ }
183
+ jsonSchemas.set(schema, json);
184
+ return json;
185
+ }
186
+ const hasType = (schema, type) => Array.isArray(schema.type) ? schema.type.includes(type) : schema.type === type;
187
+ /**
188
+ * Which branch of a union the value is for.
189
+ *
190
+ * A discriminated union says so with a `const` on the discriminator, which
191
+ * is what a form posts as a string and can be matched before coercion.
192
+ * Otherwise the first branch whose type the value could be.
193
+ */
194
+ function branchFor(branches, value) {
195
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
196
+ const record = value;
197
+ const byDiscriminator = branches.find((branch) => Object.entries(branch.properties ?? {}).some(([key, prop]) => prop.const !== undefined && record[key] === String(prop.const)));
198
+ if (byDiscriminator)
199
+ return byDiscriminator;
200
+ }
201
+ return (branches.find((branch) => typeof value === "string"
202
+ ? hasType(branch, "string") || hasType(branch, "number") || hasType(branch, "integer") || hasType(branch, "boolean")
203
+ : Array.isArray(value)
204
+ ? hasType(branch, "array")
205
+ : hasType(branch, "object")) ?? null);
206
+ }
207
+ /**
208
+ * A form's values as the types its schema means.
209
+ *
210
+ * Everything a form posts is a string or a file, and a control that is off
211
+ * is not there at all - so a schema written for the shape it wants, `boolean`
212
+ * or `number`, refused a form that was perfectly filled in, and the guide
213
+ * asked for `z.enum(['on', '1']).optional().transform(...)` per checkbox. The
214
+ * schema knows what it means; this reads the form the way the schema does:
215
+ *
216
+ * - a `boolean` that is absent is `false` (an unchecked box posts nothing);
217
+ * `"on"`, `"1"`, `"true"` are true, `"0"`, `"off"`, `"false"` are false
218
+ * - a `number` or `integer` given as a numeric string is the number; an
219
+ * empty string for one that is not required is absent
220
+ * - an `array` given one value is that value in a list, and one given
221
+ * nothing - no checkbox ticked - is the empty list
222
+ * - an object nests, a list of objects nests per item, and a union takes the
223
+ * branch its discriminator names
224
+ *
225
+ * Nothing else is touched: a string stays the string it was, a file stays a
226
+ * file, and a value the schema does not describe is passed through as posted.
227
+ */
228
+ export function coerceToSchema(value, schema) {
229
+ if (!schema)
230
+ return value;
231
+ const branches = schema.anyOf ?? schema.oneOf;
232
+ if (branches) {
233
+ const branch = branchFor(branches, value);
234
+ return branch ? coerceToSchema(value, branch) : value;
235
+ }
236
+ if (hasType(schema, "boolean")) {
237
+ if (value === undefined)
238
+ return false;
239
+ if (typeof value === "string") {
240
+ const v = value.trim().toLowerCase();
241
+ if (v === "on" || v === "1" || v === "true")
242
+ return true;
243
+ if (v === "" || v === "0" || v === "off" || v === "false")
244
+ return false;
25
245
  }
26
- else if (Array.isArray(val)) {
27
- for (const item of val) {
28
- formData.append(`${key}[]`, String(item));
29
- }
246
+ return value;
247
+ }
248
+ if (hasType(schema, "number") || hasType(schema, "integer")) {
249
+ if (typeof value === "string") {
250
+ const v = value.trim();
251
+ if (v === "")
252
+ return undefined;
253
+ if (/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(v))
254
+ return Number(v);
30
255
  }
31
- else {
32
- formData.append(key, String(val));
256
+ return value;
257
+ }
258
+ if (hasType(schema, "array")) {
259
+ // A list with nothing in it posts nothing at all: no checkbox ticked, a
260
+ // list the person emptied. That is an empty list, not a missing field.
261
+ if (value === undefined)
262
+ return [];
263
+ const list = Array.isArray(value) ? value : [value];
264
+ const items = Array.isArray(schema.items) ? null : schema.items;
265
+ return items ? list.map((item) => coerceToSchema(item, items)) : list;
266
+ }
267
+ if (hasType(schema, "object") || schema.properties) {
268
+ if (value === null || typeof value !== "object" || Array.isArray(value))
269
+ return value;
270
+ const record = { ...value };
271
+ const required = schema.required ?? [];
272
+ for (const [key, prop] of Object.entries(schema.properties ?? {})) {
273
+ // A control left blank posts "", and a field the schema does not
274
+ // require is then absent, whatever its type: z.email().optional()
275
+ // would refuse "" and the person typed nothing; a union with a
276
+ // boolean branch would read "" as false and write a value nobody
277
+ // chose. Decided before coercion, so no branch gets to. A required
278
+ // field keeps its "" so the schema can say it is required.
279
+ const coerced = record[key] === "" && !required.includes(key) ? undefined : coerceToSchema(record[key], prop);
280
+ if (coerced === undefined)
281
+ delete record[key];
282
+ else
283
+ record[key] = coerced;
33
284
  }
285
+ return record;
34
286
  }
35
- return formData;
287
+ return value;
288
+ }
289
+ /**
290
+ * A FormData as the object a schema expects, typed the way the schema means.
291
+ *
292
+ * What `<Form>` validates and what an action decodes: the same call, so a
293
+ * form that passes in the browser passes on the server. Without a schema, or
294
+ * with one that cannot describe itself, the values are the strings the form
295
+ * posted.
296
+ */
297
+ export function decodeFormData(formData, schema) {
298
+ const values = formDataToObject(formData);
299
+ return coerceToSchema(values, jsonSchemaOf(schema));
36
300
  }
37
301
  //# sourceMappingURL=formEncoding.js.map