@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -104,6 +104,40 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
104
104
  _rotateStateCookie(): void;
105
105
  /** @internal Set the keepClientCache() directive header on the response. */
106
106
  _setKeepCacheDirective(): void;
107
+ /**
108
+ * @internal This request carries the fresh-reads cookie: it follows an
109
+ * updateTag()/revalidateTag() by the same user, so the stores read tag
110
+ * markers and shells from the store, not from an isolate memo.
111
+ */
112
+ _freshReads?: boolean;
113
+ /**
114
+ * @internal Set the fresh-reads cookie on this response for `maxAgeMs`
115
+ * (rounded up to whole seconds; the longest stays). Called by
116
+ * updateTag()/revalidateTag().
117
+ */
118
+ _setFreshReadsCookie(maxAgeMs: number): void;
119
+ /**
120
+ * @internal The context createRequestContext() returned. A context derived
121
+ * from it with Object.create (a PPR HIT tail, a shell capture, a background
122
+ * revalidation render) inherits it, so per-request invalidation state
123
+ * (tag-invalidation.ts, the stores' masks) is keyed by the request, not by
124
+ * whichever derived object called or read (#973).
125
+ */
126
+ _requestRoot?: RequestContext<any, any>;
127
+ /**
128
+ * @internal The tags this request invalidated with
129
+ * updateTag()/revalidateTag(), each with its position in the invalidation
130
+ * order (tag-invalidation.ts currentCacheSeq). Kept on _requestRoot. A
131
+ * "use cache" execution that started before one of its own tags was
132
+ * invalidated is neither joined nor written (cache-runtime.ts, #973).
133
+ */
134
+ _tagInvalidations?: Map<string, number>;
135
+ /**
136
+ * @internal The handler has handed the response to the host (rsc/handler.ts
137
+ * `rango.response`): header and cookie writes after this are lost, as from
138
+ * a streaming loader or render.
139
+ */
140
+ _responseSent?: boolean;
107
141
  /**
108
142
  * Access loader data or push handle data.
109
143
  *
@@ -129,8 +163,13 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
129
163
  };
130
164
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
131
165
  method: string;
132
- /** @internal Handle store for tracking handle data across segments */
133
- _handleStore: HandleStore;
166
+ /**
167
+ * @internal Handle store for tracking handle data across segments.
168
+ * Readonly: a background render that needs its own store derives a context
169
+ * (rerenderAndCacheRoute, shell-capture.ts) instead of swapping this field,
170
+ * which the foreground still reads while it produces the page.
171
+ */
172
+ readonly _handleStore: HandleStore;
134
173
  /**
135
174
  * @internal transition({ when }) predicates for segments matched this request,
136
175
  * keyed by segment id. Collected during resolution (the function is stripped
@@ -178,15 +217,52 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
178
217
  /**
179
218
  * @internal Bake-lane loader containers collected DURING a shell capture:
180
219
  * segment-key -> the loader's (pre-wrap) result promise. Populated by
181
- * resolveLoaderData for loaders on entries with no renderable loading() (the
182
- * bake lane — they execute at capture instead of being masked; see
183
- * docs/design/loader-container-bake.md). Drained by captureAndStoreShell
184
- * after the shell quiesces: settled containers are promise-elided,
185
- * Flight-serialized, and pinned into the snapshot's loader family; a
186
- * REJECTED container refuses the capture (error UI must never bake into the
187
- * shared shell). Own property of the capture's derived context only.
220
+ * resolveLoaderData for bake-lane loaders (lane rule: see its JSDoc in
221
+ * loader-cache.ts). Drained by captureAndStoreShell after the shell
222
+ * quiesces: settled containers are promise-elided, Flight-serialized, and
223
+ * pinned into the snapshot's loader family; a REJECTED container refuses
224
+ * the capture (error UI must never bake into the shared shell). Own
225
+ * property of the capture's derived context only.
188
226
  */
189
227
  _shellCaptureLoaderRecords?: Map<string, Promise<unknown>>;
228
+ /**
229
+ * @internal Set on a capture's derived context when a loader pushed a
230
+ * value the record cannot keep (a deferred push, masked nested promises,
231
+ * or a push outside a loader body): the loader records then carry
232
+ * `runs: 1`, so a HIT still runs their bodies for those pushes.
233
+ */
234
+ _shellCaptureUnrecordedLoaderPush?: true;
235
+ /**
236
+ * @internal Record keys resolved for this request (CacheScope
237
+ * resolveKeyFrom): by the scope's `key()` (one result) or its store's
238
+ * keyGenerator (one per default key). A shell capture's derived context
239
+ * reads it through the prototype chain, so the capture reuses the
240
+ * partition its request resolved instead of running `key()` again.
241
+ */
242
+ _resolvedCacheKeys?: Map<unknown, Map<string, Promise<string>>>;
243
+ /**
244
+ * @internal The shell store key the PPR serve path resolved for this
245
+ * document request (rsc-rendering.ts shellServePlan), request partition
246
+ * included. Read by the testing helper serveShellRequest.
247
+ */
248
+ _shellKey?: string;
249
+ /**
250
+ * @internal The request carried the forced-MISS reload marker
251
+ * (shell-serve.ts SHELL_MISS_PARAM). The handler strips the marker from the
252
+ * request on entry and sets this; the shell gate renders the request like a
253
+ * cache miss.
254
+ */
255
+ _shellForcedMiss?: true;
256
+ /**
257
+ * @internal The freshness window of the route cache() records this
258
+ * request read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
259
+ * timestamps: `freshUntil` is the record's `expiresAt`, `staleUntil` adds
260
+ * its scope's swr. The narrowest record wins. A shell capture caps its
261
+ * entry's ttl and ttl+swr to it (shell-capture.ts capShellWindow), so a
262
+ * shell is never fresher, or served longer, than the route cache() entry
263
+ * it replayed or wrote.
264
+ */
265
+ _routeRecordWindow?: RouteRecordWindow;
190
266
  /**
191
267
  * @internal Loader-family snapshot seed for a shell HIT's tail render:
192
268
  * segment-key -> the capture's elided container (already Flight-deserialized
@@ -194,21 +270,24 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
194
270
  * overlays it onto the fresh run's container (recorded paths pinned,
195
271
  * hole-marker paths keep the fresh nested promises) so the payload's baked
196
272
  * bytes match the frozen prelude; a hole-free entry resolves pin-first
197
- * without gating on the fresh run. Own property of the HIT tail's derived
198
- * context only.
273
+ * and does not run the loader. Two owners: serveShellHit sets it on the
274
+ * HIT tail's derived context, and matchPartialWithPprReplay sets it on the
275
+ * request context of a PPR partial replay once its doc record hits, and
276
+ * restores the previous value after the match.
199
277
  */
200
278
  _shellLoaderSeed?: Map<string, import("../cache/shell-snapshot.js").ShellLoaderSeedEntry>;
201
279
  /**
202
- * @internal Shell fast-path marker: makes the next eligible match treat the
203
- * whole matched route as an implicit doc-level cache() boundary (see
280
+ * @internal The doc record marker: makes the next match treat the whole
281
+ * matched route as an implicit doc-level cache() boundary (see
204
282
  * resolveShellImplicitCacheScope in cache/cache-scope.ts). Set ONLY on
205
283
  * (a) the capture's derived context — with a record-only store so the
206
284
  * capture's cacheRoute write lands in the snapshot, never the real store —
207
- * (b) a HIT tail's seeded context, and (c) an eligible normal-route partial
208
- * replay, where `store` is a request-local segment overlay. Handler-live holes
209
- * and conditional transitions decline replay.
210
- * Routes with their own cache() config are never overridden — their scope's
211
- * store/key/ttl/swr/condition semantics stay authoritative. On the
285
+ * (b) a document HIT tail's seeded context (`docTail`), and (c) an eligible
286
+ * normal-route partial replay, where `store` is a request-local segment
287
+ * overlay.
288
+ * A document HIT tail replays the record even for a route with its own
289
+ * cache() config (the gate before the commit already evaluated that
290
+ * scope's opt-outs). Elsewhere the route's scope stays authoritative. On the
212
291
  * navigation-replay serve path (`onExplicitHit` set) the marker COMPOSES with
213
292
  * such a scope instead of being ignored: the seeded doc record supplies the match
214
293
  * only when the explicit tier misses (withCacheLookup). cache(false) and a
@@ -225,12 +304,17 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
225
304
  * segment record even when the triggering request is partial.
226
305
  */
227
306
  keyPrefix?: "doc";
228
- /** @internal Called only after the implicit cache hit decodes successfully. */
229
- onHit?: () => void;
307
+ /**
308
+ * @internal Called only after the implicit cache hit decodes
309
+ * successfully; the lookup awaits it before returning the hit, so work
310
+ * it starts lands before the route's loaders resolve.
311
+ */
312
+ onHit?: () => void | Promise<void>;
230
313
  /**
231
314
  * @internal Called when the seeded document record fails server-side
232
315
  * deserialization. Partial replay uses it to replace the enclosing shell
233
- * snapshot after serving the fresh fallback.
316
+ * snapshot after serving the fresh fallback. A document HIT tail does not
317
+ * set it: withCacheLookup throws ShellRecordUnavailableError instead.
234
318
  */
235
319
  onCorrupt?: () => void;
236
320
  /**
@@ -241,6 +325,21 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
241
325
  * require the exact consumable record instead of "any segment record".
242
326
  */
243
327
  docKey?: string;
328
+ /**
329
+ * @internal Set on a document HIT tail (serveShellHit). The implicit doc
330
+ * scope replaces any route-derived cache() scope for the tail's match
331
+ * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
332
+ * ShellRecordUnavailableError instead of resolving segments
333
+ * (withCacheLookup): a HIT never runs a handler.
334
+ */
335
+ docTail?: true;
336
+ /**
337
+ * @internal The key a document HIT tail looks the doc record up by: the
338
+ * entry's own `docKey`. Recomputing it could name another key (a store
339
+ * keyGenerator folding request data, a build-time capture's synthetic
340
+ * host) and miss the record.
341
+ */
342
+ fixedDocKey?: string;
244
343
  /**
245
344
  * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
246
345
  * serve path. Its presence arms the explicit-scope composition in
@@ -282,41 +381,19 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
282
381
  */
283
382
  _shellFragmentPayload?: boolean;
284
383
  /**
285
- * @internal Handler-layer liveness observed DURING a shell capture, from
286
- * three sources: (a) the capture handle-store push wrapper (shell-capture.ts)
287
- * when a push made OUTSIDE a DSL loader scope carries a nested thenable
288
- * (masked to a never-filling hole); (b) still-pending top-level handler
289
- * pushes (liveness unknowable at the barrier); (c) a handler-invoked loader
290
- * executing during the capture (loader-resolution.ts — its consumption-lane
291
- * value would freeze on a handler-free HIT). captureAndStoreShell folds it
292
- * into ShellCacheEntry.handlerLiveHoles at the putShell barrier. Own
293
- * property of the capture's derived context only.
294
- */
295
- _shellCaptureHandleLiveness?: {
296
- holes: boolean;
297
- pendingPushes: number;
298
- handlerInvokedLoader: boolean;
299
- };
300
- /**
301
- * @internal Handle values pushed from a DSL loader scope DURING a shell
302
- * capture (identity set; populated by the capture push wrapper in
303
- * shell-capture.ts). cacheRoute threads it into captureHandles so those
304
- * values stay out of cache-write handle records — loaders re-run fresh on
305
- * every HIT, so replaying their captured (masked) values would duplicate
306
- * the fresh push and stall the Flight handle encode. Own property of the
307
- * capture's derived context only; render-time handle consumers are
308
- * unaffected (the exclusion applies only at the captureHandles call site).
309
- */
310
- _shellCaptureLoaderHandleValues?: WeakSet<object>;
311
- /**
312
- * @internal Set (to the offending fn name) by the cookies()/headers()
313
- * capture guard when it throws DURING a capture render. Load-bearing for the
384
+ * @internal Set by the capture guard (server/context.ts
385
+ * tripShellCaptureGuard) when it throws DURING a capture render: the read
386
+ * ("cookies()", "headers()", "ctx.get()", "ctx.theme") and the fix the
387
+ * refusal warning gives for it. Load-bearing for the
314
388
  * bake lane: a guard throw inside an executing loader is swallowed by
315
389
  * wrapLoaderPromise into per-loader error UI, which would otherwise bake
316
390
  * silently into the shared shell — the capture checks this flag after the
317
391
  * render and refuses instead. Deterministic, so the capture does not retry.
318
392
  */
319
- _shellCaptureGuardTripped?: string;
393
+ _shellCaptureGuardTripped?: {
394
+ surface: string;
395
+ fix: string;
396
+ };
320
397
  /**
321
398
  * @internal The loader $$id whose BODY was executing when the capture guard
322
399
  * tripped (read off the loader-body ALS scope at trip time), or undefined
@@ -343,6 +420,21 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
343
420
  * content used and can therefore be invalidated by updateTag()/revalidateTag().
344
421
  */
345
422
  _requestTags: Set<string>;
423
+ /**
424
+ * @internal This request can write a route cache() record: its match
425
+ * resolved a cache scope (match-api.ts). Arms the record tag owners
426
+ * (cache-tag.ts armRecordTagOwners); off, the segment scopes, loader links
427
+ * and owned tag sets are skipped entirely. A capture's or background
428
+ * re-render's derived context (Object.create) inherits it.
429
+ */
430
+ _recordTagOwners?: true;
431
+ /**
432
+ * @internal This request resolves a loader bound with its own cache()
433
+ * (match-api.ts, or the binding's funnel). Arms the per-execution loader
434
+ * tag sets (cache-tag.ts armLoaderTagSets); off, a loader execution
435
+ * allocates no set and a read links nothing. Derived contexts inherit it.
436
+ */
437
+ _recordLoaderTags?: true;
346
438
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
347
439
  _cacheProfiles?: Record<string, import("../cache/profile-registry.js").CacheProfile>;
348
440
  /**
@@ -383,6 +475,11 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
383
475
  * Returns the theme value from the cookie, or the default theme if not set.
384
476
  * This is the user's preference ("light", "dark", or "system"), not the resolved value.
385
477
  *
478
+ * The visitor's cookie, so it is guarded like `cookies()`: reading it
479
+ * refuses a ppr shell capture and throws inside a `cache()` boundary or a
480
+ * `"use cache"` function (#971). A non-enumerable getter: a spread or
481
+ * `Object.assign` copy of the context does not carry it.
482
+ *
386
483
  * @example
387
484
  * ```typescript
388
485
  * route("settings", (ctx) => {
@@ -391,7 +488,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
391
488
  * });
392
489
  * ```
393
490
  */
394
- theme?: Theme;
491
+ readonly theme?: Theme;
395
492
  /**
396
493
  * Set the theme (only available when theme is enabled in router config)
397
494
  *
@@ -412,6 +509,14 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
412
509
  setTheme?: (theme: Theme) => void;
413
510
  /** @internal Theme configuration (null if theme not enabled) */
414
511
  _themeConfig?: ResolvedThemeConfig | null;
512
+ /**
513
+ * @internal The theme cookie (or the default) read with no identity guard.
514
+ * The guarded reads (`theme` here, the handler and middleware `ctx.theme`)
515
+ * call it after assertThemeReadAllowed; the router's own payload read goes
516
+ * through payloadInitialTheme (rsc/full-payload.ts). Undefined without
517
+ * theme config.
518
+ */
519
+ _readTheme(): Theme | undefined;
415
520
  /**
416
521
  * Attach location state entries to the current response.
417
522
  *
@@ -535,6 +640,16 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
535
640
  _renderBarrierGuardClosed?: boolean;
536
641
  /** @internal Per-request error dedup set for onError reporting */
537
642
  _reportedErrors: WeakSet<object>;
643
+ /**
644
+ * @internal Errors this response's Flight or Fizz render reported through
645
+ * onError: a component that threw does not fail the stream, which completes
646
+ * with an error row or an errored Suspense boundary. Whole-response captures
647
+ * (document-cache.ts, captureAndStoreShell) store nothing while it is
648
+ * non-empty (issue #915). An array so a render under a derived context (the
649
+ * shell HIT tail) pushes into the request's own list through the prototype;
650
+ * deriveShellCaptureContext gives the capture its own.
651
+ */
652
+ _renderErrors?: unknown[];
538
653
  /**
539
654
  * @internal Report a non-fatal background error through the router's
540
655
  * onError callback. Wired by the RSC handler / router during request
@@ -592,13 +707,27 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
592
707
  */
593
708
  _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
594
709
  }
710
+ /**
711
+ * @internal The freshness window of a route cache() record, as ms
712
+ * timestamps: fresh until `freshUntil` (its `expiresAt`), stale until
713
+ * `staleUntil` (plus its scope's swr). `ttl`, `swr` and `written` describe
714
+ * the record that sets `staleUntil`: its cache() config, and whether this
715
+ * request wrote it (cacheRoute) rather than read it.
716
+ */
717
+ export interface RouteRecordWindow {
718
+ freshUntil: number;
719
+ staleUntil: number;
720
+ ttl: number;
721
+ swr: number;
722
+ written: boolean;
723
+ }
595
724
  /**
596
725
  * Public view of RequestContext, without internal methods and fields.
597
726
  *
598
727
  * This is the type exported to library consumers. Internal code should
599
728
  * use the full RequestContext interface directly.
600
729
  */
601
- export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_pprTransitionDecisions" | "_pprReplayPostMatchReason" | "_cacheStore" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureHandleLiveness" | "_shellCaptureLoaderHandleValues" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
730
+ export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhen" | "_pprTransitionDecisions" | "_pprReplayPostMatchReason" | "_cacheStore" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureUnrecordedLoaderPush" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_routeRecordWindow" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_locationState" | "_routeName" | "_prevRouteKey" | "_gateCurrentUrl" | "_gateCurrentParams" | "_gateActionId" | "_gateActionUrl" | "_gateActionResult" | "_gateFormData" | "_inActionRevalidation" | "_reportedErrors" | "_renderErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_tagInvalidations" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
602
731
  /**
603
732
  * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
604
733
  * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
@@ -20,6 +20,7 @@ interface RenderToReadableStreamOptions {
20
20
  nonce?: string;
21
21
  formState?: unknown;
22
22
  progressiveChunkSize?: number;
23
+ onError?: (error: unknown) => void;
23
24
  }
24
25
  /**
25
26
  * ReadableStream with the allReady promise added by react-dom/server.edge.
@@ -99,6 +100,12 @@ export interface SSRRenderOptions {
99
100
  * prerender pass (host-agnostic bare captures).
100
101
  */
101
102
  origin?: string;
103
+ /**
104
+ * Called for each error Fizz reports through its onError: a component that
105
+ * threw inside a Suspense boundary, which leaves the boundary errored in an
106
+ * otherwise completed document. React's default console.error is kept.
107
+ */
108
+ onError?: (error: unknown) => void;
102
109
  }
103
110
  /**
104
111
  * SSR dependencies from external packages
@@ -225,6 +232,21 @@ interface ShellCaptureOptions {
225
232
  * capture, resume, and browser hydration.
226
233
  */
227
234
  origin?: string;
235
+ /**
236
+ * Called for each component error the prerender reports (an errored
237
+ * boundary in the prelude), never for the capture's own abort. The caller
238
+ * refuses to store the shell (issue #915).
239
+ */
240
+ onError?: (error: unknown) => void;
241
+ /**
242
+ * Called with React's errorInfo for each task still pending when the
243
+ * capture's own abort froze the prelude; its componentStack is computed
244
+ * when read. The dev no-shell warning prints the stacks: one of them is what
245
+ * suspended above <body> (issue #941).
246
+ */
247
+ onAbortedTask?: (errorInfo: {
248
+ componentStack?: string;
249
+ } | undefined) => void;
228
250
  }
229
251
  /**
230
252
  * Result of a successful shell capture. `prelude` is the raw prelude bytes;
@@ -253,6 +275,8 @@ interface ShellResumeOptions {
253
275
  search?: string;
254
276
  /** The HIT request's origin — same host as the capture's (key-scoped). */
255
277
  origin?: string;
278
+ /** Called for each component error the resumed holes report. */
279
+ onError?: (error: unknown) => void;
256
280
  }
257
281
  /**
258
282
  * Create an SSR handler that converts RSC streams to HTML.
@@ -54,14 +54,15 @@ export interface SsrRootOptions {
54
54
  nonce?: string;
55
55
  /**
56
56
  * Fires once when the Flight payload root settles (resolve OR reject) — the
57
- * signal that every client-module load the payload references completed and
58
- * fizz can start emitting the tree. The capture pass gates its abort on
59
- * this: a fully REPLAYED (prerendered) route's Flight stream goes
60
- * byte-quiet in ~1-3ms, but fizz cannot render even <html> until the
61
- * module loads finish (real module-runner I/O in dev; 100ms+ on a cold
62
- * graph), so an abort gated on Flight quiet alone fires first and freezes
63
- * a zero-byte prelude. Masked-loader holes do NOT block this signal —
64
- * they postpone below the root.
57
+ * signal that fizz can start emitting the tree. A client component used as
58
+ * an element type resolves lazily, so its module load can still be in
59
+ * flight (captureShellHTML waits for those separately, issue #949). The
60
+ * capture pass gates its abort on this: a fully REPLAYED (prerendered)
61
+ * route's Flight stream goes byte-quiet in ~1-3ms, but fizz cannot render
62
+ * even <html> until the module loads finish (real module-runner I/O in dev;
63
+ * 100ms+ on a cold graph), so an abort gated on Flight quiet alone fires
64
+ * first and freezes a zero-byte prelude. Masked-loader holes do NOT block
65
+ * this signal — they postpone below the root.
65
66
  */
66
67
  onPayloadSettled?: () => void;
67
68
  /**
@@ -21,11 +21,14 @@
21
21
  * ```ts
22
22
  * export const DocsNav = Static((ctx) => <Nav docs={readDocsSync()} />);
23
23
  * export const DocShell = Static((ctx) => <Shell />);
24
+ * // Prerender/Static must be exported consts; an inline call inside path()
25
+ * // gets no build-injected $$id and throws.
26
+ * export const DocPage = Prerender(getParams, DocPageHandler);
24
27
  *
25
28
  * urls(({ path, layout }) => [
26
29
  * layout(DocsNav, () => [
27
30
  * path("/getting-started", DocShell, { name: "doc.gs" }),
28
- * path("/:slug", Prerender(getParams, DocPageHandler)),
31
+ * path("/:slug", DocPage),
29
32
  * ]),
30
33
  * ]);
31
34
  * ```
@@ -18,6 +18,11 @@
18
18
  * element tree, so you can assert typed prop fidelity across the client boundary
19
19
  * (a `Date` comes back a `Date`) and detect inlined-vs-island. Serialize +
20
20
  * deserialize only — no hydration/interaction (that is the e2e tier).
21
+ *
22
+ * `serveShellRequest` serves one request through the router's production
23
+ * handler, so a `ppr` route's MISS runs the real shell capture and a later
24
+ * request is a real HIT; only the HTML step is stubbed. `resetShellTestState`
25
+ * clears the per-isolate PPR state between tests.
21
26
  */
22
27
  export { renderToFlightString, normalizeFlight, assertFlightRuntimeAvailable, } from "./flight.js";
23
28
  export type { RenderToFlightStringOptions } from "./flight.js";
@@ -25,3 +30,5 @@ export { renderServerTree, findClientBoundaries, findElements, textContent, asse
25
30
  export type { RenderServerTreeOptions, RenderServerTreeResult, ClientBoundary, BoundarySelector, FoundElement, ElementSelector, } from "./flight-tree.js";
26
31
  export { renderHandler } from "./render-handler.js";
27
32
  export type { TestableHandler, RenderHandlerOptions, RenderHandlerResult, StateCookieSeed, } from "./render-handler.js";
33
+ export { serveShellRequest, resetShellTestState, } from "./serve-shell-request.js";
34
+ export type { ServeShellRequestOptions, ServeShellRequestResult, } from "./serve-shell-request.js";
@@ -56,10 +56,10 @@ export declare function diffGeneratedRoutes(router: RouterWithRouteMap, generate
56
56
  *
57
57
  * @example
58
58
  * ```ts
59
- * import generated from "./router.named-routes.gen";
59
+ * import { NamedRoutes } from "./router.named-routes.gen";
60
60
  * import { router } from "./router";
61
61
  *
62
- * await assertGeneratedRoutesMatch(router, generated);
62
+ * await assertGeneratedRoutesMatch(router, NamedRoutes);
63
63
  * ```
64
64
  */
65
65
  export declare function assertGeneratedRoutesMatch(router: RouterWithRouteMap, generatedMap?: Record<string, unknown>): Promise<void>;
@@ -205,7 +205,7 @@ export declare function buildRunSnapshot<TEnv>(ctx: RequestContext<TEnv>, thrown
205
205
  *
206
206
  * @example
207
207
  * ```ts
208
- * const { result, cookies, response, thrown } = await runInRequestContext(
208
+ * const { cookies, headers, response, thrown } = await runInRequestContext(
209
209
  * () => loginAction(input), // sets a session cookie, then `throw redirect("/app")`
210
210
  * {
211
211
  * env,
@@ -23,10 +23,13 @@
23
23
  * SEEDED directly into client context (see the `loaders` / `locationState` /
24
24
  * `handles` / `outletPending` options) — nothing is executed on the server.
25
25
  * This exercises the context read path, not the run path.
26
- * - navigate() commits synchronously, so it does NOT drive the navigation
27
- * lifecycle: useNavigation().state, useLinkStatus().pending, and
28
- * useAction().state stay "idle". Assert pending/loading/submitting transition
29
- * states with renderServerTree / e2e instead (navigate() warns once if used).
26
+ * - navigate() does NOT drive the navigation lifecycle: useNavigation().state,
27
+ * useLinkStatus().pending, and useAction().state stay "idle". Assert
28
+ * pending/loading/submitting transition states with renderServerTree / e2e
29
+ * instead (navigate() warns once if used). The one pending state it does
30
+ * model is the held-navigation useLoader().isLoading: a `transition` spec
31
+ * plus a pending Promise seeded via navigate(url, { loaders }) commits
32
+ * through production's commitInTransition (browser/partial-update.ts).
30
33
  * What it DOES cover: client hooks that read NavigationProvider /
31
34
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
32
35
  * useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
@@ -42,7 +45,7 @@ import { type ComponentType } from "react";
42
45
  import type { RenderResult } from "@testing-library/react";
43
46
  import type { NavigationStore } from "../browser/types.js";
44
47
  import type { EventController } from "../browser/event-controller.js";
45
- import type { LoaderDefinition } from "../types.js";
48
+ import type { LoaderDefinition, TransitionConfig } from "../types.js";
46
49
  import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
47
50
  import type { Handle } from "../handle.js";
48
51
  import type { ThemeConfig } from "../theme/types.js";
@@ -78,6 +81,14 @@ export interface RenderRouteSpec {
78
81
  loaderIds?: string[];
79
82
  /** Optional route name (informational; not used for matching). */
80
83
  name?: string;
84
+ /**
85
+ * The `transition()` config this node declares (`{}` for a bare
86
+ * `transition()`), attached to its segment as the DSL does. Puts the chain in
87
+ * a transition scope: param-agnostic keys (segment-system.tsx) and a held
88
+ * navigate() commit, so a pending loader seed passed to
89
+ * `router.navigate(url, { loaders })` pins `useLoader().isLoading`.
90
+ */
91
+ transition?: TransitionConfig;
81
92
  }
82
93
  /**
83
94
  * Options for renderRoute.
@@ -247,8 +258,23 @@ export interface TestRouterHandle {
247
258
  * updates params + location, and re-renders the segment tree. This is a
248
259
  * client-only navigation: no server fetch occurs, so only the components in
249
260
  * `routes` can be reached.
261
+ *
262
+ * `options.loaders` seeds loader data for THIS navigation, merged over the
263
+ * render-time seeds (the data a real navigation's response would carry). A
264
+ * pending Promise suspends the read like a streaming Flight chunk. When a
265
+ * spec in the chain has `transition`, the commit goes through production's
266
+ * commitInTransition: the reader stays on screen with
267
+ * `useLoader().isLoading === true` until the promise settles. Settle it
268
+ * inside RTL's `act()` to flush the commit. Without `transition` the commit
269
+ * is urgent and the read suspends to the nearest Suspense boundary.
270
+ *
271
+ * @example
272
+ * await router.navigate("/products/2", { loaders: [[ProductLoader, next]] });
273
+ * // reader: isLoading true, old data
274
+ * await act(async () => resolve({ name: "Product 2" }));
275
+ * // reader: isLoading false, new data
250
276
  */
251
- navigate(url: string): Promise<void>;
277
+ navigate(url: string, options?: Pick<RenderRouteOptions, "loaders">): Promise<void>;
252
278
  /** The current committed pathname. */
253
279
  pathname(): string;
254
280
  /** The current committed params. */
@@ -38,6 +38,7 @@ import { type Handle } from "../handle.js";
38
38
  import type { ThemeConfig } from "../theme/types.js";
39
39
  import type { SegmentCacheStore } from "../cache/types.js";
40
40
  import type { CacheProfile } from "../cache/profile-registry.js";
41
+ import type { PartialCacheOptions } from "../types/cache-types.js";
41
42
  import { type VarsInit, type StateCookieSeed } from "./internal/context.js";
42
43
  /**
43
44
  * The loader context surfaced to a `runLoader` body. It mirrors the runtime
@@ -81,9 +82,11 @@ export interface RunLoaderOptions<TEnv = any> {
81
82
  basename?: string;
82
83
  /**
83
84
  * Theme config in the same shape `createRouter({ theme })` takes (e.g. `true`
84
- * or `{ themes: [...] }`). Seeds the request's theme config so nested handler
85
- * or cache contexts created from this loader observe it. Loaders themselves do
86
- * not expose `ctx.theme`/`ctx.setTheme` (those are handler/middleware-only).
85
+ * or `{ themes: [...] }`). Seeds the request's theme config, so the loader
86
+ * body reads the visitor's theme (the Cookie on `request`, or the default)
87
+ * with `getRequestContext().theme`. That read is guarded like `cookies()`: it
88
+ * throws inside a `"use cache"` function (#971). The loader ctx has no
89
+ * `theme`/`setTheme` (those are handler/middleware-only).
87
90
  */
88
91
  theme?: ThemeConfig | true;
89
92
  /** Environment bindings surfaced as `ctx.env`. */
@@ -121,6 +124,18 @@ export interface RunLoaderOptions<TEnv = any> {
121
124
  cacheStore?: SegmentCacheStore;
122
125
  /** Cache profiles (the `createRouter({ cacheProfiles })` shape). */
123
126
  cacheProfiles?: Record<string, CacheProfile>;
127
+ /**
128
+ * Run the loader through the `cache()` a route binds it with — the options
129
+ * `loader(Loader, () => [cache({...})])` takes — using the production
130
+ * read-through against `cache.store`, else `cacheStore`. A HIT returns the
131
+ * stored value without running the body and records the tags the body
132
+ * recorded when the entry was written, so `updateTag()` of any of them drops
133
+ * the entry. The write is a background task the call does not await (spy
134
+ * on the store's `setItem` to wait for it). A raw loader body is keyed by
135
+ * its function reference. `handlePushes` lists the body's pushes, so a HIT
136
+ * reports none: the replay into the page's handle store is not modeled.
137
+ */
138
+ cache?: PartialCacheOptions;
124
139
  /**
125
140
  * Customize the rango state cookie a loader that calls
126
141
  * `invalidateClientCache()` rotates (the name is always seeded — default