@rangojs/router 0.15.1 → 0.17.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 (249) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +52 -0
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { RouterError } from "../errors.js";
10
10
  import { getRequestContext } from "../server/request-context.js";
11
- import { contextGet } from "../context-var.js";
11
+ import { contextGet, isNonCacheable } from "../context-var.js";
12
12
  import { NOCACHE_SYMBOL } from "../cache/taint.js";
13
13
  import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
14
14
  import { createCacheScope, resolveCacheTags } from "../cache/cache-scope.js";
@@ -19,7 +19,10 @@ import {
19
19
  stripInternalParams,
20
20
  } from "../router/handler-context.js";
21
21
  import type { MiddlewareFn } from "../router/middleware.js";
22
- import type { EntryData } from "../server/context.js";
22
+ import {
23
+ assertNonCacheableReadAllowed,
24
+ type EntryData,
25
+ } from "../server/context.js";
23
26
  import type { HandlerContext } from "./handler-context.js";
24
27
  import { createProblemDetails } from "./response-error.js";
25
28
  import {
@@ -85,7 +88,12 @@ export async function handleResponseRoute<TEnv>(
85
88
  originalUrl: reqCtx.originalUrl,
86
89
  pathname: url.pathname,
87
90
  reverse: createReverseFunction(handlerCtx.getRequiredRouteMap()),
88
- get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
91
+ get: ((keyOrVar: any) => {
92
+ if (isNonCacheable(variables, keyOrVar)) {
93
+ assertNonCacheableReadAllowed(keyOrVar);
94
+ }
95
+ return contextGet(variables, keyOrVar);
96
+ }) as any,
89
97
  header: (name: string, value: string) => reqCtx.header(name, value),
90
98
  waitUntil: reqCtx.waitUntil.bind(reqCtx),
91
99
  executionContext: reqCtx.executionContext,
@@ -2,8 +2,7 @@
2
2
  * RSC Rendering Handler (Navigation)
3
3
  *
4
4
  * Handles RSC rendering for both partial (client-side navigation) and full
5
- * (initial page load) requests. Includes prerender collection for build-time
6
- * static generation.
5
+ * (initial page load) requests.
7
6
  */
8
7
 
9
8
  import {
@@ -394,8 +393,8 @@ function* requestRenderPlan<TEnv>(
394
393
  const shell = yield* scope("shell-serve", shellServePlan(input));
395
394
  if (shell.kind === "serve") return shell.response;
396
395
 
397
- // Match + payload. A control response (redirect, prerender collection)
398
- // ends the plan before any rendering.
396
+ // Match + payload. A control response (redirect) ends the plan before any
397
+ // rendering.
399
398
  const prepared = yield* scope(
400
399
  isPartial ? "prepare:partial" : "prepare:full",
401
400
  preparePayloadPlan(input),
@@ -470,7 +469,6 @@ function* shellServePlan<TEnv>(
470
469
  if (
471
470
  isPartial ||
472
471
  request.method !== "GET" ||
473
- url.searchParams.has("__prerender_collect") ||
474
472
  isRscRequest(request, url, false) ||
475
473
  reqCtx._dynamic
476
474
  ) {
@@ -639,7 +637,7 @@ function* shellServePlan<TEnv>(
639
637
 
640
638
  /**
641
639
  * Match the request and assemble the RSC payload, or produce a control
642
- * response (redirect, prerender collection) that ends the request plan.
640
+ * response (redirect) that ends the request plan.
643
641
  */
644
642
  function* preparePayloadPlan<TEnv>(
645
643
  input: RequestRenderInput<TEnv>,
@@ -750,13 +748,6 @@ function* preparePayloadPlan<TEnv>(
750
748
  // Caching is now handled in router.match() via cache provider in request context
751
749
  // match.segments already contains cached or fresh segments as appropriate
752
750
 
753
- if (url.searchParams.has("__prerender_collect")) {
754
- const response = yield* step("prerender-collect", () =>
755
- collectPrerenderArtifacts(match, handleStore),
756
- );
757
- return { kind: "control", response };
758
- }
759
-
760
751
  return {
761
752
  kind: "payload",
762
753
  payload: buildFullPayload(match, ctx, url, reqCtx, handleStore),
@@ -869,38 +860,6 @@ function mirrorPprServerTimingsForDev(
869
860
  }
870
861
  }
871
862
 
872
- /**
873
- * Build-time prerender collection: serialize segments and handle data
874
- * to JSON for storage as build artifacts. At runtime the worker
875
- * deserializes these and feeds them through the normal segment pipeline.
876
- */
877
- async function collectPrerenderArtifacts<TEnv>(
878
- match: Awaited<ReturnType<HandlerContext<TEnv>["router"]["match"]>>,
879
- handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
880
- ): Promise<Response> {
881
- const nonLoaderSegments = match.segments.filter((s) => s.type !== "loader");
882
- handleStore.seal();
883
- await handleStore.settled;
884
- const { serializeSegments } = await import("../cache/segment-codec.js");
885
- const serializedSegments = await serializeSegments(nonLoaderSegments);
886
- const handles: Record<string, Record<string, unknown[]>> = {};
887
- for (const seg of nonLoaderSegments) {
888
- const segHandles = handleStore.getDataForSegment(seg.id);
889
- if (Object.keys(segHandles).length > 0) {
890
- handles[seg.id] = segHandles;
891
- }
892
- }
893
- return new Response(
894
- JSON.stringify({
895
- segments: serializedSegments,
896
- handles,
897
- routeName: match.routeName,
898
- params: match.params,
899
- }),
900
- { headers: { "Content-Type": "application/json" } },
901
- );
902
- }
903
-
904
863
  /** Assemble headers + tracking and run the foreground stage-driver render. */
905
864
  function renderPreparedRscResponse<TEnv>(
906
865
  input: RequestRenderInput<TEnv>,
@@ -1534,6 +1493,10 @@ function serveShellHit(
1534
1493
  search: shellSearchSeed(url, reqCtx._searchParamsFilter),
1535
1494
  // The HIT request's origin — same host as the capture's (key-scoped).
1536
1495
  origin: url.origin,
1496
+ // The document cache reads the list before storing this composite.
1497
+ onError: (error) => {
1498
+ reqCtx._renderErrors?.push(error);
1499
+ },
1537
1500
  }),
1538
1501
  );
1539
1502
  };
@@ -43,3 +43,16 @@ export const SHELL_CAPTURE_MAX_WAIT_MS = 15_000;
43
43
  * check on the stampede guard, keyed off the same constant.
44
44
  */
45
45
  export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
46
+
47
+ /**
48
+ * Shared PPR loader-lane hint, appended to every capture warning or error
49
+ * whose fix depends on the lane (shell-capture.ts warnings, the cookie-store.ts
50
+ * capture guard). Lives in this import-free leaf so cookie-store.ts can import
51
+ * it without pulling the capture orchestration module. Source anchor for the
52
+ * rule: resolveLoaderData (router/segment-resolution/loader-cache.ts).
53
+ */
54
+ export const PPR_LANE_HINT: string =
55
+ "Lane rule: only loader(Def, { ssr: false }) executes at capture and bakes its " +
56
+ "non-promise data; every other loader is live (masked at capture, fresh per " +
57
+ "request) and needs loading() or an inline <Suspense> above its reader. See the " +
58
+ '/ppr skill, "The loader lane rule" (node_modules/@rangojs/router/skills/ppr/SKILL.md).';
@@ -28,6 +28,7 @@ import {
28
28
  enqueueSerializedCapture,
29
29
  } from "./capture-queue.js";
30
30
  import {
31
+ PPR_LANE_HINT,
31
32
  SHELL_CAPTURE_MAX_WAIT_MS,
32
33
  SHELL_CAPTURE_TASK_HARD_CAP_MS,
33
34
  } from "./shell-capture-constants.js";
@@ -257,10 +258,11 @@ function releaseCaptureGuard(key: string, token: CaptureGuardToken): void {
257
258
  * mode's ceiling (60s in production, {@link REFUSED_CAPTURE_DEV_MAX_MS} in dev).
258
259
  *
259
260
  * Why exponential and not a flat 60s: a flat long window conflates two very
260
- * different failures. A STRUCTURALLY ineligible route (no loading(), a cookie
261
- * reader) fails forever and wants the long 60s cap. But a cold-but-ELIGIBLE route
262
- * can also fail the in-place retry under a truly cold graph (dev module transform,
263
- * or a cold worker under parallel load) — and it must recover FAST, on the next
261
+ * different failures. A STRUCTURALLY ineligible route (a boundary-less live
262
+ * loader read, a cookie reader) fails forever and wants the long 60s cap. But a
263
+ * cold-but-ELIGIBLE route can also fail the in-place retry under a truly cold
264
+ * graph (dev module transform, or a cold worker under parallel load) — and it
265
+ * must recover FAST, on the next
264
266
  * request or two, not be frozen for 60s (that would re-break the very cold-start DX
265
267
  * the retry fixes; it bit the cloudflare dev e2e). Escalating from 1s means the
266
268
  * eligible route re-probes almost immediately (warm now → HIT and clear), while the
@@ -366,9 +368,8 @@ const warnedNullCaptures = new Set<string>();
366
368
  * usually healed, so a firing warning leans toward the structural cause — but we
367
369
  * still name both so a cold-start straggler is not misdiagnosed.
368
370
  *
369
- * The pointer is shipped-path-safe (a05c8251 convention): the /ppr skill ships in
370
- * the npm tarball, but docs/design/ is repo-only, so link it by absolute GitHub URL
371
- * rather than a relative path that dead-ends for consumers.
371
+ * The pointer is the shared PPR_LANE_HINT: the /ppr skill ships in the npm
372
+ * tarball, so the path resolves for consumers (a05c8251 convention).
372
373
  */
373
374
  function warnNullCaptureOnce(key: string): void {
374
375
  if (warnedNullCaptures.has(key)) return;
@@ -379,18 +380,13 @@ function warnNullCaptureOnce(key: string): void {
379
380
  "by whether the route ever flips to HIT:\n" +
380
381
  " 1. Cold-start warmup (dev module transform, or a cold worker): the capture raced " +
381
382
  "an unfinished shell render. This SELF-HEALS — the route flips to HIT once a later " +
382
- "request warms the modules. Usually nothing to do.\n" +
383
+ "request warms the modules.\n" +
383
384
  " 2. Something suspends above <body> with no Suspense boundary and never settles " +
384
- "within the capture window: a slower-than-the-capture-guard bake-lane loader " +
385
- "(loaders on entries WITHOUT loading() execute at capture and their containers " +
386
- "bake — the boundary-less await must settle for a shell to exist), or a pending " +
387
- "promise consumed without a <Suspense> above it. The boundary belongs on the " +
388
- "entry/component that OWNS the data: loading() on the entry that registers the " +
389
- "loader (a child route's loading() does not unpin a parent layout's loaders), or " +
390
- "a <Suspense> above the consuming component.\n" +
391
- 'See the /ppr skill (node_modules/@rangojs/router/skills/ppr/SKILL.md), "The hole ' +
392
- 'doctrine" and "The layout-with-loaders playbook", or the design docs: ' +
393
- "https://github.com/rangojs/rango/blob/main/packages/rangojs-router/docs/design/ppr-shell-resume.md",
385
+ "within the capture window: a live loader (no ssr: false) read without loading() or " +
386
+ "an inline <Suspense>; a loader(Def, { ssr: false }) or top-level push (e.g. Meta) " +
387
+ "slower than ppr.captureTimeout; or a pending promise with no Suspense above it. The " +
388
+ "boundary belongs to the entry or component that owns the data.\n" +
389
+ PPR_LANE_HINT,
394
390
  );
395
391
  }
396
392
 
@@ -399,19 +395,23 @@ const warnedRefusedCaptures = new Set<string>();
399
395
 
400
396
  /**
401
397
  * Warn once per key that the capture was REFUSED for a deterministic reason
402
- * (identity-guard trip or a rejected bake-lane loader). Distinct from
403
- * warnNullCaptureOnce: these are not cold-start shapes, the retry is skipped,
404
- * and the message carries the concrete cause instead of a differential.
398
+ * (identity-guard trip, a rejected or signal-settled bake-lane loader, a store
399
+ * refusal). Distinct from warnNullCaptureOnce: these are not cold-start
400
+ * shapes, the retry is skipped, and the message carries the concrete cause
401
+ * instead of a differential. Lane-dependent causes pass PPR_LANE_HINT as the
402
+ * trailing pointer; the rest keep the plain skill pointer.
405
403
  */
406
- function warnCaptureRefusedOnce(key: string, reason: string): void {
404
+ function warnCaptureRefusedOnce(
405
+ key: string,
406
+ reason: string,
407
+ pointer: string = "See the /ppr skill (node_modules/@rangojs/router/skills/ppr/SKILL.md).",
408
+ ): void {
407
409
  if (warnedRefusedCaptures.has(key)) return;
408
410
  warnedRefusedCaptures.add(key);
409
411
  console.warn(
410
412
  `[rango] Shell capture for "${key}" was refused: ${reason}\n` +
411
413
  "The route stays on MISS (axis 1) — the page keeps working, only the shell " +
412
- "cache is off. See the /ppr skill " +
413
- "(node_modules/@rangojs/router/skills/ppr/SKILL.md) and " +
414
- "docs/design/loader-container-bake.md.",
414
+ `cache is off. ${pointer}`,
415
415
  );
416
416
  }
417
417
 
@@ -489,13 +489,11 @@ function warnBakeCostOnce(key: string, source: string, ms: number): void {
489
489
  `[rango] Shell capture for "${key}" waited ${ms}ms for ${source} to ` +
490
490
  "settle before the shell could freeze. This cost recurs on every capture " +
491
491
  "of the route and occupies the per-isolate capture queue; the served " +
492
- "response never shows it. Top-level promises BAKE by design (the " +
493
- "container settles; nested promises stay live). To make the value " +
494
- "per-request instead, nest it ({ data: promise }) and read it with use() " +
495
- "under Suspense; to keep it baked but cheap, wrap the work in cache() " +
496
- "(the capture replays the cached value); for a segment loader, add a " +
497
- "loading() boundary to its entry (live lane, masked at capture). See the " +
498
- "/ppr skill (node_modules/@rangojs/router/skills/ppr/SKILL.md).",
492
+ "response never shows it. To make the value per-request, nest it " +
493
+ "({ data: promise }) and read it with use() under Suspense; to keep it " +
494
+ "baked but cheap, wrap the work in cache(); for a loader(Def, { ssr: false }), " +
495
+ "drop ssr: false to move it to the live lane. " +
496
+ PPR_LANE_HINT,
499
497
  );
500
498
  }
501
499
 
@@ -1328,8 +1326,9 @@ async function runShellCapture(
1328
1326
  if (second !== "no-shell") return second;
1329
1327
 
1330
1328
  // Both attempts came back with no usable shell. Cold-start would have healed by
1331
- // now, so the eternal-MISS structural shape (a loader route without loading()) is
1332
- // the likely cause — warn once per key. Ordering matters: because the retry
1329
+ // now, so the eternal-MISS structural shape (a boundary-less live-loader read;
1330
+ // lane rule: see resolveLoaderData, loader-cache.ts) is the likely cause —
1331
+ // warn once per key. Ordering matters: because the retry
1333
1332
  // absorbs cold-start, cold-start routes almost never reach this warning. The
1334
1333
  // caller (scheduleShellCapture) reads this `no-shell` return to back the key off.
1335
1334
  log(
@@ -1507,29 +1506,34 @@ export function deriveShellCaptureContext(
1507
1506
  // re-run can fill, so the entry must not serve handler-free
1508
1507
  // (ShellCacheEntry.handlerLiveHoles). Still-pending top-level handler
1509
1508
  // pushes at the putShell barrier count too — their liveness is unknowable.
1510
- // - loaderScopedPushValues: DSL-loader pushes re-run fresh on every HIT, so
1511
- // their captured values must NOT enter a segment record's handle snapshot
1509
+ // - loader-push tag: DSL-loader pushes re-run fresh on every HIT, so their
1510
+ // captured values must NOT enter a segment record's handle snapshot
1512
1511
  // (replay would duplicate the fresh push, and their masked nested
1513
- // promises would stall the Flight handle encode to its timeout). The set
1514
- // rides the derived context (_shellCaptureLoaderHandleValues) and is
1515
- // applied ONLY at the captureHandles cache-write call site — every other
1516
- // getDataForSegment consumer (the render-barrier snapshot, prerender)
1517
- // sees every push.
1512
+ // promises would stall the Flight handle encode to its timeout). The
1513
+ // store tags them by array position (HandleStore.push loaderPush), which
1514
+ // covers primitives, and only the captureHandles cache-write call site
1515
+ // excludes tagged pushes — every other getDataForSegment consumer (the
1516
+ // render-barrier snapshot, prerender) sees every push. Unflagged loaders
1517
+ // never execute at capture; the tagged pushes come from loaders an
1518
+ // ssr:false loader awaits via ctx.use and from loader-cache replays
1519
+ // (loader-cache.ts replayLoaderHandles), both re-run on a HIT.
1518
1520
  // EXCEPTION — bake-lane { ssr: false } loaders: they execute at capture
1519
- // and never re-run on a HIT, so neither exclusion rationale applies. The
1520
- // capture awaits them (fresh.ts), their pushes are in the captured HTML
1521
- // (<head> title/meta, useHandle echoes), and the stored snapshot MUST
1522
- // carry the same values or HIT hydration mismatches against its own
1523
- // prelude (server text present, client handle empty). Only their
1524
- // settled, thenable-free pushes qualify — a deferred (thenable) push or
1525
- // one with masked nested promises keeps the exclusion so the handle
1526
- // encode cannot stall on a never-resolving mask.
1521
+ // (fresh.ts awaits them), their pushes are in the captured HTML (<head>
1522
+ // title/meta, useHandle echoes), and the stored snapshot carries the same
1523
+ // values so a HIT's handle data is guaranteed to match its own prelude.
1524
+ // They still re-run on every HIT (resolveLoaderData, loader-cache.ts), so
1525
+ // each kept push carries its loader as `owner` (HandleStore.push): the
1526
+ // record stores it (CachedEntryData.handleOwners), the HIT restores it
1527
+ // through pushReplayed, and the re-run's live pushes replace the
1528
+ // restored copies in place. Only settled, thenable-free pushes
1529
+ // qualify — a deferred (thenable) push or one with masked nested promises
1530
+ // keeps the exclusion so the handle encode cannot stall on a
1531
+ // never-resolving mask.
1527
1532
  const handleLiveness = {
1528
1533
  holes: false,
1529
1534
  pendingPushes: 0,
1530
1535
  handlerInvokedLoader: false,
1531
1536
  };
1532
- const loaderScopedPushValues = new WeakSet<object>();
1533
1537
  const rawCapturePush = freshHandleStore.push.bind(freshHandleStore);
1534
1538
  freshHandleStore.push = (
1535
1539
  handleName: string,
@@ -1550,42 +1554,36 @@ export function deriveShellCaptureContext(
1550
1554
  return masked;
1551
1555
  };
1552
1556
  let masked: unknown;
1557
+ let loaderPush = pushedInLoaderScope;
1558
+ let owner: string | undefined;
1553
1559
  if (isThenable(value)) {
1554
1560
  if (!pushedInLoaderScope) {
1555
1561
  handleLiveness.pendingPushes++;
1556
1562
  const settle = () => handleLiveness.pendingPushes--;
1557
1563
  value.then(settle, settle);
1558
1564
  }
1559
- masked = value.then(maskWithLiveness);
1560
- // Deferred (thenable) loader pushes always keep the exclusion — the
1565
+ // Deferred (thenable) loader pushes always keep the tag — the
1561
1566
  // bake-lane carve-out below is for settled values only.
1562
- if (pushedInLoaderScope) {
1563
- loaderScopedPushValues.add(masked as object);
1564
- }
1567
+ masked = value.then(maskWithLiveness);
1565
1568
  } else {
1566
1569
  masked = maskWithLiveness(value);
1567
- if (
1568
- pushedInLoaderScope &&
1569
- typeof masked === "object" &&
1570
- masked !== null
1571
- ) {
1570
+ if (loaderPush && !maskedNestedThenable) {
1572
1571
  // Bake-lane { ssr: false } loader pushes are shell material (see the
1573
- // funnel comment above): keep them IN the stored snapshot when
1574
- // settled and thenable-free; everything else stays excluded.
1572
+ // funnel comment above): untagged, the stored snapshot keeps them
1573
+ // when settled and thenable-free; everything else stays tagged.
1575
1574
  const bodyLoaderId = getCurrentLoaderBodyId();
1576
- const baked =
1577
- bodyLoaderId !== undefined &&
1578
- !maskedNestedThenable &&
1579
- derivedCtx._awaitBeforeFlushLoaderIds?.has(bodyLoaderId) === true;
1580
- if (!baked) {
1581
- loaderScopedPushValues.add(masked);
1582
- }
1575
+ loaderPush =
1576
+ bodyLoaderId === undefined ||
1577
+ derivedCtx._awaitBeforeFlushLoaderIds?.has(bodyLoaderId) !== true;
1578
+ if (!loaderPush) owner = bodyLoaderId;
1583
1579
  }
1584
1580
  }
1585
- rawCapturePush(handleName, segmentId, masked);
1581
+ rawCapturePush(handleName, segmentId, masked, loaderPush, owner);
1586
1582
  };
1587
1583
 
1588
- const derivedCtx: RequestContext = Object.create(reqCtx);
1584
+ const derivedCtx: RequestContext = Object.assign(Object.create(reqCtx), {
1585
+ _handleStore: freshHandleStore,
1586
+ });
1589
1587
  if (identity) {
1590
1588
  derivedCtx.request = identity.request;
1591
1589
  derivedCtx.url = identity.url;
@@ -1593,7 +1591,6 @@ export function deriveShellCaptureContext(
1593
1591
  derivedCtx.pathname = identity.url.pathname;
1594
1592
  derivedCtx.searchParams = identity.url.searchParams;
1595
1593
  }
1596
- derivedCtx._handleStore = freshHandleStore;
1597
1594
  // Own render barrier, closure-bound to the derived ctx and the fresh store
1598
1595
  // (issue #684, plan 009). Without this every _renderBarrier* read fell
1599
1596
  // through the prototype to the foreground's ALREADY-RESOLVED barrier: a
@@ -1603,7 +1600,6 @@ export function deriveShellCaptureContext(
1603
1600
  // also resets _treeHasStreaming (recomputed for the capture's tree) and the
1604
1601
  // deadlock-guard fields as own properties.
1605
1602
  wireRenderBarrier(derivedCtx, freshHandleStore);
1606
- derivedCtx._shellCaptureLoaderHandleValues = loaderScopedPushValues;
1607
1603
  derivedCtx._requestTags = new Set<string>();
1608
1604
  // Own explicit-store registry: cache-store resolutions during the capture
1609
1605
  // (the implicit scope's SnapshotOnlySegmentStore, any per-capture explicit
@@ -1614,6 +1610,9 @@ export function deriveShellCaptureContext(
1614
1610
  // context; module-singleton stores stay registered by normal renders.
1615
1611
  derivedCtx._explicitTaggedStores = new Set();
1616
1612
  derivedCtx._transitionWhen = [];
1613
+ // Own list: the capture's render errors refuse the capture only, and the
1614
+ // foreground's never reach it.
1615
+ derivedCtx._renderErrors = [];
1617
1616
  derivedCtx._shellCaptureRun = true;
1618
1617
  // Own-property reset: an inherited flag would make serializeSegments store
1619
1618
  // a double-encoded fragment. No current arming site reaches a capture
@@ -1714,8 +1713,9 @@ async function captureAndStoreShell(
1714
1713
  // generator has not yielded, SsrRoot suspends at the root (consumeAsyncGenerator
1715
1714
  // sits above every boundary), the prelude comes back trivial, and
1716
1715
  // captureShellHTML's sanity gate returns null: the designed fail-safe no-op, not
1717
- // an error. This mirrors the __prerender_collect seal+settled regime, which also
1718
- // excludes loaders. See docs/design/ppr-shell-resume.md ("Loaders and handles").
1716
+ // an error. This mirrors the matchForPrerender (router/prerender-match.ts)
1717
+ // seal+settled regime, which also excludes loaders. See
1718
+ // docs/design/ppr-shell-resume.md ("Loaders and handles").
1719
1719
  handleStore.seal();
1720
1720
 
1721
1721
  // Handles contract, shell half ("nesting = liveness"): TOP-LEVEL pushed handle
@@ -1794,13 +1794,12 @@ async function captureAndStoreShell(
1794
1794
  : "handler/render code (no loader body was executing)";
1795
1795
  warnCaptureRefusedOnce(
1796
1796
  capture.key,
1797
- `${origin} called ${fnName}() during capture. Identity must not bake into a shared shell. ` +
1798
- "For a segment loader (bake lane, no loading()): give its entry a loading() boundary " +
1799
- "(the live lane, masked at capture) or move the identity-dependent part into a nested " +
1800
- "promise. For handler/render code: keep the value live by consuming a loader " +
1801
- 'client-side (useLoader in a "use client" component). Note: `await ctx.use(loader)` ' +
1802
- "inside a HANDLER is exempt from this guard — its value bakes into the shared shell " +
1803
- "as a capture-time copy, mirroring cache() semantics (the consumption-lane rule).",
1797
+ `${origin} called ${fnName}() during capture; identity must not bake into the shared shell. ` +
1798
+ "Segment loader: drop ssr: false and read it under loading() or an inline <Suspense> " +
1799
+ "(a nested promise does not help: its body still runs at capture). Handler/render " +
1800
+ "code: read it in a live loader and consume it with useLoader. Exception: " +
1801
+ "await ctx.use(loader) in a HANDLER bakes as a capture-time copy.",
1802
+ PPR_LANE_HINT,
1804
1803
  );
1805
1804
  return "refused";
1806
1805
  };
@@ -1819,6 +1818,9 @@ async function captureAndStoreShell(
1819
1818
  // static-part search reads bake what the key names.
1820
1819
  search: capture.searchSeed,
1821
1820
  origin: capture.originSeed,
1821
+ onError: (error) => {
1822
+ reqCtx._renderErrors?.push(error);
1823
+ },
1822
1824
  }),
1823
1825
  );
1824
1826
  } catch (error) {
@@ -1835,9 +1837,9 @@ async function captureAndStoreShell(
1835
1837
  // null = sanity gate refused (trivial/empty prelude, no <body>). Store nothing
1836
1838
  // and report `no-shell` so the caller (runShellCapture) can retry once and, if
1837
1839
  // that also fails, warn once per key. On a cold render this is the shell not
1838
- // yet finished; on a loader route WITHOUT a route-level loading() boundary it is
1839
- // the structural eternal-MISS shape (the masked loader pins the tree above
1840
- // <body> at tree-build). The caller's warning names both.
1840
+ // yet finished; a boundary-less live-loader read (lane rule: see
1841
+ // resolveLoaderData, loader-cache.ts) is the structural eternal-MISS shape.
1842
+ // The caller's warning names both.
1841
1843
  // Guard check first — BEFORE the trivial-prelude retry path. A guard trip
1842
1844
  // is deterministic (retrying re-trips it), and when the tripping loader's
1843
1845
  // error UI still completed a shell, storing it would bake the failure into
@@ -1938,7 +1940,8 @@ async function captureAndStoreShell(
1938
1940
  warnCaptureRefusedOnce(
1939
1941
  capture.key,
1940
1942
  `the loader for segment "${segmentKey}" rejected during capture; its error UI must not bake into the shared shell. ` +
1941
- "Fix the loader, or give its entry a loading() boundary so it stays on the live lane.",
1943
+ "Fix the loader, or drop its ssr: false to move it to the live lane.",
1944
+ PPR_LANE_HINT,
1942
1945
  );
1943
1946
  return "refused";
1944
1947
  }
@@ -1954,7 +1957,8 @@ async function captureAndStoreShell(
1954
1957
  warnCaptureRefusedOnce(
1955
1958
  capture.key,
1956
1959
  `the loader for segment "${segmentKey}" settled with redirect()/notFound() during capture; a request-specific signal must not bake into the shared shell. ` +
1957
- "Give its entry a loading() boundary so it stays on the live lane, or move the decision into middleware.",
1960
+ "Drop its ssr: false to move it to the live lane, or move the decision into middleware.",
1961
+ PPR_LANE_HINT,
1958
1962
  );
1959
1963
  return "refused";
1960
1964
  }
@@ -1966,7 +1970,14 @@ async function captureAndStoreShell(
1966
1970
  // must round-trip; serializeResult preserves it through Flight.
1967
1971
  serializeContainer ??= (await import("../cache/segment-codec.js"))
1968
1972
  .serializeResult;
1969
- const serialized = await serializeContainer(elided.value);
1973
+ // A value Flight cannot encode (an async component that throws, a
1974
+ // rejected promise the elide walk does not reach, a function, a
1975
+ // class instance) completes with an error row instead of rejecting
1976
+ // (issue #927). Collected with the render's errors, so the check
1977
+ // after this drain refuses the capture.
1978
+ const serialized = await serializeContainer(elided.value, (error) => {
1979
+ reqCtx._renderErrors?.push(error);
1980
+ });
1970
1981
  if (serialized !== null) {
1971
1982
  (snapshot ??= []).push({
1972
1983
  family: "loader",
@@ -1979,12 +1990,22 @@ async function captureAndStoreShell(
1979
1990
  });
1980
1991
  }
1981
1992
  } catch {
1982
- // Non-serializable container: leave it unpinned (it drifts on a HIT,
1983
- // the pre-snapshot behavior) rather than failing the capture.
1993
+ // Codec import failed: leave it unpinned (it drifts on a HIT, the
1994
+ // pre-snapshot behavior) rather than failing the capture.
1984
1995
  }
1985
1996
  }
1986
1997
  }
1987
1998
 
1999
+ // A shell component that threw inside a Suspense boundary does not reject
2000
+ // the capture: Flight and Fizz report it through onError and the prelude
2001
+ // carries the errored boundary, which every HIT would serve (issue #915).
2002
+ // The drain's container encode adds its Flight errors here too (#927).
2003
+ // After the loader drain so a rejected bake-lane loader keeps its specific
2004
+ // refusal. Thrown like a fatal shell error: no retry, reportCacheError and
2005
+ // backoff in scheduleShellCapture.
2006
+ const renderErrors = reqCtx._renderErrors;
2007
+ if (renderErrors && renderErrors.length > 0) throw renderErrors[0];
2008
+
1988
2009
  // Snapshot size guard (issue #651): the snapshot duplicates every pinned
1989
2010
  // cache value inside the shell entry, so a page over a large cache()
1990
2011
  // segment can push the stored envelope toward store value limits (KV caps
@@ -82,11 +82,11 @@ export interface SsrHtmlStageOptions<TEnv> {
82
82
  /** Metrics store for ssr:module-load / ssr:stream-mode timing (per call site). */
83
83
  metricsStore: MetricsStore | undefined;
84
84
  /**
85
- * renderHTML options minus streamMode (the stage resolves streamMode). Carries
86
- * the per-site nonce and, for the PE action re-render, formState. Spread
87
- * verbatim so each call site's exact key set is preserved.
85
+ * renderHTML options minus streamMode and onError (the stage owns both).
86
+ * Carries the per-site nonce and, for the PE action re-render, formState.
87
+ * Spread verbatim so each call site's exact key set is preserved.
88
88
  */
89
- render: Omit<SSRRenderOptions, "streamMode">;
89
+ render: Omit<SSRRenderOptions, "streamMode" | "onError">;
90
90
  /** ResponseInit merged into the prepared render (e.g. content-type). */
91
91
  init?: ResponseInit;
92
92
  }
@@ -109,6 +109,7 @@ export function createSsrHtmlStage<TEnv>(
109
109
  options.url,
110
110
  options.metricsStore,
111
111
  );
112
+ const renderErrors = _getRequestContext()?._renderErrors;
112
113
  return {
113
114
  render: () =>
114
115
  // search: the LIVE request's query string, threaded out-of-band into
@@ -124,6 +125,12 @@ export function createSsrHtmlStage<TEnv>(
124
125
  // so origin-dependent markup (Link's data-external) agrees with the
125
126
  // browser's window.location across hydration.
126
127
  origin: options.url.origin,
128
+ // A component that throws inside a Suspense boundary leaves an
129
+ // errored boundary in a completed document; the document cache
130
+ // reads the list before storing it.
131
+ onError: (error) => {
132
+ renderErrors?.push(error);
133
+ },
127
134
  }),
128
135
  ...(options.init && { init: options.init }),
129
136
  };
@@ -185,8 +192,8 @@ function acceptsFlightExplicitly(request: Request, url: URL): boolean {
185
192
  * Classify whether a request may require SSR (HTML rendering).
186
193
  *
187
194
  * Returns false for requests that are definitively RSC-only: transport
188
- * params (partial/action/loader/__rsc), prerender collection, or an explicit
189
- * Accept: text/x-component. Must never return false for a request whose
195
+ * params (partial/action/loader/__rsc) or an explicit Accept:
196
+ * text/x-component. Must never return false for a request whose
190
197
  * render-time decision (isRscRequest) will be HTML — the two share
191
198
  * acceptsFlightExplicitly so the Accept rule cannot drift. document-cache.ts
192
199
  * keys its HTML/RSC response slots off this function, so any divergence from
@@ -201,8 +208,7 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
201
208
  url.searchParams.has("_rsc_action") ||
202
209
  request.headers.has("rsc-action") ||
203
210
  url.searchParams.has("_rsc_loader") ||
204
- url.searchParams.has("__rsc") ||
205
- url.searchParams.has("__prerender_collect")
211
+ url.searchParams.has("__rsc")
206
212
  ) {
207
213
  return false;
208
214
  }
package/src/rsc/types.ts CHANGED
@@ -193,6 +193,13 @@ export interface SSRRenderOptions {
193
193
  * the browser across hydration. Absent on host-agnostic build captures.
194
194
  */
195
195
  origin?: string;
196
+
197
+ /**
198
+ * Called for each error Fizz reports through its onError: a component that
199
+ * threw inside a Suspense boundary, which leaves the boundary errored in an
200
+ * otherwise completed document. React's default console.error is kept.
201
+ */
202
+ onError?: (error: unknown) => void;
196
203
  }
197
204
 
198
205
  /**
@@ -219,6 +226,8 @@ export interface SSRModule {
219
226
  maxWaitMs?: number;
220
227
  search?: string;
221
228
  origin?: string;
229
+ /** Each component error the prerender reports (never the capture's own abort). */
230
+ onError?: (error: unknown) => void;
222
231
  },
223
232
  ) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
224
233
 
@@ -237,6 +246,8 @@ export interface SSRModule {
237
246
  nonce?: string;
238
247
  search?: string;
239
248
  origin?: string;
249
+ /** Each component error the resumed holes report. */
250
+ onError?: (error: unknown) => void;
240
251
  },
241
252
  ) => Promise<ReadableStream<Uint8Array>>;
242
253
  }