@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RSC-environment version of client exports
3
3
  *
4
- * This file is used when importing "rsc-router/client" from RSC (server components).
4
+ * This file is used when importing "@rangojs/router/client" from RSC (server components).
5
5
  * It re-exports the server's createLoader so that loader definitions work in both
6
6
  * environments with the same import.
7
7
  *
@@ -9,6 +9,10 @@
9
9
  * in RSC context, while the regular client.tsx is used in client components.
10
10
  */
11
11
 
12
+ // From html.ts directly, never via ./client.js: every export of that
13
+ // "use client" module is an opaque client reference here.
14
+ import * as Html from "./html.js";
15
+
12
16
  // Re-export everything from client.tsx (Outlet, useLoader, etc.)
13
17
  // These are safe to use in RSC context
14
18
  export {
@@ -64,7 +68,10 @@ export {
64
68
  useLinkStatus,
65
69
  type LinkStatus,
66
70
  } from "./browser/react/use-link-status.js";
67
- export { useScrollRestoration } from "./browser/react/ScrollRestoration.js";
71
+ export {
72
+ useScrollRestoration,
73
+ type ScrollRestorationProps,
74
+ } from "./browser/react/ScrollRestoration.js";
68
75
 
69
76
  export {
70
77
  Link,
@@ -72,11 +79,6 @@ export {
72
79
  type PrefetchStrategy,
73
80
  } from "./browser/react/Link.js";
74
81
 
75
- export {
76
- ScrollRestoration,
77
- type ScrollRestorationProps,
78
- } from "./browser/react/ScrollRestoration.js";
79
-
80
82
  export {
81
83
  NavigationProvider,
82
84
  type NavigationProviderProps,
@@ -91,14 +93,13 @@ export { MountContext } from "./browser/react/mount-context.js";
91
93
  export { createHandle, isHandle, type Handle } from "./handle.js";
92
94
 
93
95
  export { Meta } from "./handles/meta.js";
94
- export { MetaTags } from "./handles/MetaTags.js";
95
96
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
96
97
  export {
97
98
  Script,
98
99
  type ScriptConfig,
99
100
  type ScriptAttributes,
100
101
  } from "./handles/script.js";
101
- export { Scripts } from "./handles/Scripts.js";
102
+ export { Html };
102
103
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
103
104
 
104
105
  export {
package/src/client.tsx CHANGED
@@ -8,6 +8,9 @@ import {
8
8
  Suspense,
9
9
  type ReactNode,
10
10
  } from "react";
11
+ // Not `export * as Html`: plugin-rsc's "use client" proxy transform only
12
+ // expands bare `export *` and throws on a named one.
13
+ import * as Html from "./html.js";
11
14
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
12
15
  import {
13
16
  type ClientErrorBoundaryFallbackProps,
@@ -300,7 +303,7 @@ interface ErrorBoundaryState {
300
303
  * @example
301
304
  * ```tsx
302
305
  * "use client";
303
- * import { ErrorBoundary } from "rsc-router/client";
306
+ * import { ErrorBoundary } from "@rangojs/router/client";
304
307
  *
305
308
  * function MyComponent() {
306
309
  * return (
@@ -427,7 +430,6 @@ export {
427
430
  } from "./browser/react/use-link-status.js";
428
431
 
429
432
  export {
430
- ScrollRestoration,
431
433
  useScrollRestoration,
432
434
  type ScrollRestorationProps,
433
435
  } from "./browser/react/ScrollRestoration.js";
@@ -436,14 +438,13 @@ export { type Handle } from "./handle.js";
436
438
  export { useHandle } from "./browser/react/use-handle.js";
437
439
 
438
440
  export { Meta } from "./handles/meta.js";
439
- export { MetaTags } from "./handles/MetaTags.js";
440
441
  export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
441
442
  export {
442
443
  Script,
443
444
  type ScriptConfig,
444
445
  type ScriptAttributes,
445
446
  } from "./handles/script.js";
446
- export { Scripts } from "./handles/Scripts.js";
447
+ export { Html };
447
448
  export { Breadcrumbs, type BreadcrumbItem } from "./handles/breadcrumbs.js";
448
449
 
449
450
  export {
@@ -3,12 +3,16 @@
3
3
  import type { ReactNode, ReactElement } from "react";
4
4
  import { MetaTags } from "../handles/MetaTags.js";
5
5
  import { Scripts } from "../handles/Scripts.js";
6
+ import { ScrollRestoration } from "../browser/react/ScrollRestoration.js";
6
7
 
7
8
  /**
8
9
  * Default document component that provides a basic HTML structure.
9
10
  * Used when no custom document is provided to createRouter.
10
- * Includes MetaTags for automatic charset, viewport, and route meta support,
11
- * and Scripts (head + body sites) so the Script handle works out of the box.
11
+ * Renders every Html member: Html.Meta (charset, viewport, route meta), head +
12
+ * body Html.Scripts sites for the Script handle, and Html.ScrollRestoration.
13
+ * An app that wants a custom getKey or no scroll restoration passes its own
14
+ * document; rendering a second Html.ScrollRestoration under this one only
15
+ * warns ("[Scroll] Already initialized").
12
16
  *
13
17
  * Uses suppressHydrationWarning on <html> because the theme script
14
18
  * may modify class/style attributes before React hydrates.
@@ -27,6 +31,7 @@ export function DefaultDocument({
27
31
  <body>
28
32
  <Scripts position="body" />
29
33
  {children}
34
+ <ScrollRestoration />
30
35
  </body>
31
36
  </html>
32
37
  );
@@ -12,7 +12,7 @@
12
12
  * interface PaginationData { current: number; total: number }
13
13
  * export const Pagination = createVar<PaginationData>();
14
14
  *
15
- * // Non-cacheable var — ctx.get(User) throws inside a cache() boundary
15
+ * // Non-cacheable var — ctx.get(User) throws inside cache() or "use cache"
16
16
  * export const User = createVar<UserData>({ cache: false });
17
17
  *
18
18
  * // handler
@@ -26,7 +26,7 @@
26
26
  export interface ContextVar<T> {
27
27
  readonly __brand: "context-var";
28
28
  readonly key: symbol;
29
- /** When false, ctx.get(var) throws inside a cache() boundary. */
29
+ /** When false, ctx.get(var) throws inside cache() or "use cache". */
30
30
  readonly cache: boolean;
31
31
  /** Phantom field to carry the type parameter. Never set at runtime. */
32
32
  readonly __type?: T;
@@ -35,9 +35,10 @@ export interface ContextVar<T> {
35
35
  export interface ContextVarOptions {
36
36
  /**
37
37
  * When false, marks this variable as non-cacheable.
38
- * Reading this var with ctx.get() inside a cache() boundary throws. Use for
39
- * inherently request-specific data (user sessions, auth tokens, etc.) that
40
- * must never be baked into cached segments.
38
+ * Reading this var with ctx.get() inside a cache() boundary or a "use cache"
39
+ * function throws. Use for inherently request-specific data (user sessions,
40
+ * auth tokens, etc.) that must never be baked into cached segments or
41
+ * entries; pass the value into a "use cache" function as an argument.
41
42
  *
42
43
  * @default true
43
44
  */
@@ -45,7 +45,7 @@ export function escapeJsonForScript(json: string): string {
45
45
  * source; for untrusted dynamic data, JSON-encode it and read it back, rather than
46
46
  * inlining it as code.
47
47
  *
48
- * Used by the Script handle's <Scripts> renderer for inline `children`.
48
+ * Used by the Script handle's <Html.Scripts> renderer for inline `children`.
49
49
  */
50
50
  export function escapeScriptBody(js: string): string {
51
51
  return js.replace(/<!--/g, "<\\u0021--").replace(/<\/(script)/gi, "<\\/$1");
@@ -2,17 +2,18 @@
2
2
 
3
3
  /**
4
4
  * Component to render collected meta descriptors in the document head.
5
+ * Public as `Html.Meta` (`@rangojs/router/client`).
5
6
  *
6
- * Deferred (Promise) meta descriptors are resolved before MetaTags renders
7
+ * Deferred (Promise) meta descriptors are resolved before `<Html.Meta />` renders
7
8
  * (server-side on the full render, client-side before apply on navigation), so
8
9
  * it only ever receives resolved descriptors and never suspends.
9
10
  *
10
- * When theme is enabled in the router config, MetaTags also renders
11
+ * When theme is enabled in the router config, `<Html.Meta />` also renders
11
12
  * the theme initialization script to prevent FOUC (flash of unstyled content).
12
- * This makes MetaTags the sole FOUC-script injector for apps that render it;
13
- * the standalone `<ThemeScript />` is only needed when MetaTags is not used.
14
- * Rendering both is safe (the inline script guards listener registration) but
15
- * redundant.
13
+ * This makes `<Html.Meta />` the sole FOUC-script injector for apps that render
14
+ * it; the standalone `<ThemeScript />` is only needed when `<Html.Meta />` is not
15
+ * used. Rendering both is safe (the inline script guards listener registration)
16
+ * but redundant.
16
17
  *
17
18
  * @example
18
19
  * ```tsx
@@ -20,7 +21,7 @@
20
21
  * return (
21
22
  * <html lang="en" suppressHydrationWarning>
22
23
  * <head>
23
- * <MetaTags />
24
+ * <Html.Meta />
24
25
  * </head>
25
26
  * <body>...</body>
26
27
  * </html>
@@ -179,17 +180,18 @@ export function renderMetaDescriptor(
179
180
  }
180
181
 
181
182
  /**
182
- * Renders all collected meta descriptors from route handlers.
183
+ * Renders all collected meta descriptors from route handlers. Public as
184
+ * `Html.Meta` (`@rangojs/router/client`).
183
185
  *
184
- * Place this component inside the `<head>` element of your document.
186
+ * Place `<Html.Meta />` inside the `<head>` element of your document.
185
187
  * It will automatically update when meta descriptors change during navigation.
186
188
  *
187
189
  * When theme is enabled in router config, also renders the theme initialization
188
190
  * script to prevent FOUC (flash of unstyled content).
189
191
  *
190
- * Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
192
+ * Deferred (Promise) meta descriptors are resolved BEFORE this renders —
191
193
  * server-side on the full/SSR render, client-side before apply on navigation
192
- * (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
194
+ * (resolve-by-default) — so it only ever receives resolved descriptors and
193
195
  * never suspends.
194
196
  */
195
197
  export function MetaTags(): React.ReactNode {
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * Renders the scripts collected by the Script handle into the document.
4
+ * Renders the scripts collected by the Script handle into the document. Public
5
+ * as `Html.Scripts` (`@rangojs/router/client`).
5
6
  *
6
- * Place `<Scripts />` inside `<head>` (default) and, if you push body scripts,
7
- * `<Scripts position="body" />` at the top of `<body>`. Each site renders the
8
- * configs whose `position` matches; the request CSP nonce is applied
7
+ * Place `<Html.Scripts />` inside `<head>` (default) and, if you push body
8
+ * scripts, `<Html.Scripts position="body" />` at the top of `<body>`. Each site
9
+ * renders the configs whose `position` matches; the request CSP nonce is applied
9
10
  * automatically to every DOCUMENT-RENDERED <script> (consumers never pass it). An
10
11
  * async script first encountered on a soft navigation is injected client-side
11
12
  * where the nonce is unavailable, so it carries no nonce and relies on
@@ -24,11 +25,11 @@
24
25
  * ```tsx
25
26
  * <html>
26
27
  * <head>
27
- * <MetaTags />
28
- * <Scripts />
28
+ * <Html.Meta />
29
+ * <Html.Scripts />
29
30
  * </head>
30
31
  * <body>
31
- * <Scripts position="body" />
32
+ * <Html.Scripts position="body" />
32
33
  * {children}
33
34
  * </body>
34
35
  * </html>
@@ -80,10 +81,10 @@ function passthroughAttributes(
80
81
  if (dev) {
81
82
  console.warn(
82
83
  isHandler
83
- ? `[Scripts] event handler "${key}" in a script's attributes is ` +
84
+ ? `[Html.Scripts] event handler "${key}" in a script's attributes is ` +
84
85
  `dropped; callbacks cannot cross the server -> client handle ` +
85
86
  `boundary. Use a "use client" component for load/error handling.`
86
- : `[Scripts] managed field "${key}" in a script's attributes is ` +
87
+ : `[Html.Scripts] managed field "${key}" in a script's attributes is ` +
87
88
  `dropped; set it via the ScriptConfig fields (the request nonce ` +
88
89
  `is applied automatically).`,
89
90
  );
@@ -129,7 +130,7 @@ function renderScript(
129
130
  async
130
131
  ) {
131
132
  console.warn(
132
- `[Scripts] An async external script (src="${src}") is hoisted into ` +
133
+ `[Html.Scripts] An async external script (src="${src}") is hoisted into ` +
133
134
  `<head> by React; position: "body" is ignored for it.`,
134
135
  );
135
136
  }
@@ -19,7 +19,7 @@
19
19
  * return (
20
20
  * <html>
21
21
  * <head>
22
- * <MetaTags />
22
+ * <Html.Meta />
23
23
  * </head>
24
24
  * ...
25
25
  * </html>
@@ -225,7 +225,7 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
225
225
  * Built-in handle for managing document metadata.
226
226
  *
227
227
  * Use `ctx.use(Meta)` in route handlers to push meta descriptors.
228
- * Use `<MetaTags />` component to render them in the document head.
228
+ * Use the `<Html.Meta />` component to render them in the document head.
229
229
  *
230
230
  * Deferred (Promise) descriptors are resolved before collectMeta runs
231
231
  * (resolve-by-default), so they participate in deduplication and `%s`
@@ -3,22 +3,23 @@
3
3
  * route/layout handlers.
4
4
  *
5
5
  * Push from a SERVER handler with `ctx.use(Script)(config)`; render with the
6
- * `<Scripts />` component (from `@rangojs/router/client`) placed in the Document
7
- * `<head>` (and optionally a second `<Scripts position="body" />` at the top of
8
- * `<body>`). This mirrors the Meta / <MetaTags> pair.
6
+ * `<Html.Scripts />` component (from `@rangojs/router/client`) placed in the
7
+ * Document `<head>` (and optionally a second `<Html.Scripts position="body" />`
8
+ * at the top of `<body>`). This mirrors the Meta / <Html.Meta> pair.
9
9
  *
10
- * The request CSP nonce is applied AUTOMATICALLY by <Scripts> to document-rendered
11
- * scripts; consumers never pass a nonce. (An async script first loaded on a soft
12
- * navigation is injected client-side without a nonce — it relies on
13
- * 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT below and the
14
- * /scripts skill.) A ScriptConfig is fully serializable (it crosses the
15
- * server -> client handle-collection boundary), so callbacks like onLoad are NOT
16
- * supported — a consumer needing them renders their own "use client" script.
10
+ * The request CSP nonce is applied AUTOMATICALLY by <Html.Scripts> to
11
+ * document-rendered scripts; consumers never pass a nonce. (An async script
12
+ * first loaded on a soft navigation is injected client-side without a nonce —
13
+ * it relies on 'strict-dynamic' or a host allowance; see the EXECUTION CONTRACT
14
+ * below and the /scripts skill.) A ScriptConfig is fully serializable (it
15
+ * crosses the server -> client handle-collection boundary), so callbacks like
16
+ * onLoad are NOT supported — a consumer needing them renders their own
17
+ * "use client" script.
17
18
  *
18
19
  * EXECUTION CONTRACT (see the /scripts skill for the full story):
19
20
  * - Inline (`children`) and ordered external (`src`, optional `defer`) scripts
20
21
  * are DOCUMENT-LOAD scripts: they execute only when present in the initial HTML
21
- * response. <Scripts> freezes them after hydration, so a later client (soft)
22
+ * response. <Html.Scripts> freezes them after hydration, so a later client (soft)
22
23
  * navigation never inserts an inert copy — React creates client-mounted
23
24
  * <script> elements via innerHTML, which the HTML spec makes non-executing.
24
25
  * - Async external scripts (`src` + `async: true`) are React RESOURCES: they load
@@ -78,9 +79,9 @@ export type ScriptAttributes = Omit<
78
79
  /** Fields shared by every script shape. */
79
80
  interface ScriptConfigBase {
80
81
  /**
81
- * Where <Scripts> renders this script.
82
- * - "head" (default): the `<head>` <Scripts> site.
83
- * - "body": the `<Scripts position="body" />` site at the top of <body>.
82
+ * Where <Html.Scripts> renders this script.
83
+ * - "head" (default): the `<head>` <Html.Scripts> site.
84
+ * - "body": the `<Html.Scripts position="body" />` site at the top of <body>.
84
85
  * Note: an external `async` script is hoisted into <head> by React regardless.
85
86
  */
86
87
  position?: "head" | "body";
@@ -96,7 +97,7 @@ interface ScriptConfigBase {
96
97
  /**
97
98
  * Inline script: a raw JS body rendered in place, escaped against `</script>`
98
99
  * breakout. DOCUMENT-LOAD only (executes when present in the initial HTML;
99
- * <Scripts> freezes it after hydration so navigation never inserts an inert
100
+ * <Html.Scripts> freezes it after hydration so navigation never inserts an inert
100
101
  * copy). `id` is REQUIRED — inline scripts are never deduped by React, so a
101
102
  * layout and a child pushing the same bootstrap would inject it twice. It is also
102
103
  * rendered as the script's DOM `id`. Forbids `src`/`async`/`defer`. For analytics
@@ -146,7 +147,7 @@ export interface OrderedScriptConfig extends ScriptConfigBase {
146
147
  * inline (`id` + `children`), external async (`src` + `async: true`), or external
147
148
  * ordered (`src`, optional `defer`). Invalid combinations (both `src`+`children`,
148
149
  * `async`+`defer`, inline without `id`) are compile errors. The CSP nonce is
149
- * applied by <Scripts>, never here.
150
+ * applied by <Html.Scripts>, never here.
150
151
  */
151
152
  export type ScriptConfig =
152
153
  | InlineScriptConfig
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @example
8
8
  * ```typescript
9
- * import { href } from "rsc-router/client";
9
+ * import { href } from "@rangojs/router/client";
10
10
  *
11
11
  * href("/blog/my-post"); // ✓ matches /blog/:slug
12
12
  * href("/shop/product/widget"); // ✓ matches /shop/product/:slug
package/src/html.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Members of the `Html` namespace; client.tsx and client.rsc.tsx expose this
3
+ * module as `import * as Html`. A module namespace, not an object literal, so
4
+ * bundlers drop members an app never renders (a literal kept opt-in
5
+ * ScrollRestoration in every app: +313 B gzip, over the router-chunk ratchet).
6
+ * No "use client": each member stays its own module's client reference, so a
7
+ * server component can dot into `Html`. Pinned by __tests__/html.test.ts and
8
+ * testing/__tests__/html-namespace.rsc-test.tsx.
9
+ */
10
+ export { MetaTags as Meta } from "./handles/MetaTags.js";
11
+ export { Scripts } from "./handles/Scripts.js";
12
+ export { ScrollRestoration } from "./browser/react/ScrollRestoration.js";
package/src/index.rsc.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  LoaderDefinition,
52
52
  LoaderFn,
53
53
  LoaderContext,
54
+ LoaderOptions,
54
55
  FetchableLoaderOptions,
55
56
  LoadOptions,
56
57
  // Error boundary types
@@ -188,6 +189,9 @@ export {
188
189
  type RouterRequestInput,
189
190
  } from "./router.js";
190
191
 
192
+ // Route manifest snapshot returned by router.debugManifest()
193
+ export type { SerializedManifest } from "./debug.js";
194
+
191
195
  // RSC handler types (server-side)
192
196
  export type { HandlerCacheConfig } from "./rsc/types.js";
193
197
  export {
@@ -254,6 +258,7 @@ export type {
254
258
  export type {
255
259
  ScopedReverseFunction,
256
260
  ReverseFunction,
261
+ GlobalReverseFunction,
257
262
  ExtractLocalRoutes,
258
263
  ParamsFor,
259
264
  } from "./reverse.js";
package/src/index.ts CHANGED
@@ -334,6 +334,7 @@ export type { BreadcrumbItem } from "./handles/breadcrumbs.js";
334
334
  export type {
335
335
  ScopedReverseFunction,
336
336
  ReverseFunction,
337
+ GlobalReverseFunction,
337
338
  ExtractLocalRoutes,
338
339
  ParamsFor,
339
340
  } from "./reverse.js";
@@ -162,10 +162,13 @@ export class LoaderStore {
162
162
  * browser/partial-update.ts calls INSIDE every transition commit, so a
163
163
  * reader whose content is held on screen by that transition pins
164
164
  * `isLoading: true` for the data it is still showing until the transition
165
- * lands (use-loader.tsx). The new tree's own read of the loader suspends at
166
- * the read site and never observes the flag. A Set, not a flag: overlapping
167
- * navigations (second click before the first stream lands) keep the family
168
- * pending until the LAST stream settles.
165
+ * lands (use-loader.tsx). Family-keyed on purpose: a persisting layout
166
+ * reader of the same createLoader the child is re-running is pinned too
167
+ * (the loader is in flight; not "this segment's copy is being replaced" —
168
+ * issue #862, working as designed). The new tree's own read of the loader
169
+ * suspends at the read site and never observes the flag. A Set, not a
170
+ * flag: overlapping navigations (second click before the first stream
171
+ * lands) keep the family pending until the LAST stream settles.
169
172
  */
170
173
  private readonly pendingStreams = new Map<string, Set<Promise<unknown>>>();
171
174
 
@@ -4,9 +4,9 @@
4
4
  * Runs in the RSC realm of the build's temp server, AFTER all bundles are
5
5
  * written (the prelude embeds built client asset URLs — bootstrap module,
6
6
  * chunk preloads — that only exist post-client-build). The capture core is
7
- * producer A's, verbatim: deriveShellCaptureContext (mask funnel, liveness,
8
- * snapshot recording, implicit doc-cache scope) + captureAndStoreShell (gates,
9
- * quiesce, tags union, putShell barrier). Build capture first replays global
7
+ * producer A's, verbatim: deriveShellCaptureContext (push funnel, snapshot
8
+ * recording, implicit doc-cache scope) + settleCaptureRecord (the record-first
9
+ * step) + captureAndStoreShell (gates, quiesce, tags union, putShell barrier). Build capture first replays global
10
10
  * and route middleware with a synthetic build request context
11
11
  * (`ctx.build === true`, inert `ctx.waitUntil()`); middleware can seed vars or
12
12
  * call `ctx.dynamic()` to skip this URL. The sink is an entry collector instead
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import type { ShellCacheEntry } from "../cache/types.js";
24
+ import type { MatchResult } from "../types.js";
24
25
  import { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
25
26
  import {
26
27
  createRequestContext,
@@ -31,7 +32,10 @@ import {
31
32
  import {
32
33
  deriveShellCaptureContext,
33
34
  captureAndStoreShell,
35
+ settleCaptureRecord,
36
+ withinDeadline,
34
37
  delay,
38
+ SHELL_CAPTURE_MAX_WAIT_MS,
35
39
  SHELL_CAPTURE_RETRY_DELAY_MS,
36
40
  type ShellCaptureDescriptor,
37
41
  } from "../rsc/shell-capture.js";
@@ -44,7 +48,7 @@ import { isRouteNotFoundError } from "../errors.js";
44
48
  import { createReverseFunction } from "../router/handler-context.js";
45
49
  import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
46
50
  import type { MiddlewareEntry } from "../router/middleware.js";
47
- import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
51
+ import { getGlobalRouteMap } from "../route-map-builder.js";
48
52
  import {
49
53
  resolvePprConfig,
50
54
  type ResolvedPprConfig,
@@ -134,18 +138,25 @@ export interface BuildShellCaptureResult {
134
138
  tags?: string[];
135
139
  /** On route-mismatch: what this router's match actually landed on. */
136
140
  matchedRouteName?: string;
141
+ /**
142
+ * On no-shell: the attempt ran out of ppr.captureTimeout. It is not
143
+ * retried: its match may still be running, and the retry would start the
144
+ * same work beside it (runtime twin: shell-capture.ts runShellCapture).
145
+ */
146
+ timedOut?: true;
137
147
  }
138
148
 
139
149
  /**
140
150
  * Capture the PPR shell for one prerendered URL at build time. Retries once
141
151
  * in place on `no-shell` (the first attempt warms the temp server's SSR/Flight
142
- * transform graph, mirroring producer A's cold-start retry — same delay).
152
+ * transform graph, mirroring producer A's cold-start retry — same delay),
153
+ * unless the attempt ran out of ppr.captureTimeout (`timedOut`).
143
154
  */
144
155
  export async function captureShellForBuild(
145
156
  opts: BuildShellCaptureOptions,
146
157
  ): Promise<BuildShellCaptureResult> {
147
158
  const first = await attemptBuildCapture(opts);
148
- if (first.outcome !== "no-shell") return first;
159
+ if (first.outcome !== "no-shell" || first.timedOut) return first;
149
160
  if (opts.debug) {
150
161
  console.log(
151
162
  `[rango] shell capture attempt 1/2 for ${opts.urlPath} produced no shell (cold graph?) — retrying`,
@@ -215,6 +226,7 @@ async function attemptBuildCapture(
215
226
  };
216
227
 
217
228
  let mismatchedRouteName: string | undefined;
229
+ let timedOut = false;
218
230
  const result = await runWithRequestContext(baseCtx, async () => {
219
231
  const preview =
220
232
  typeof router.previewMatch === "function"
@@ -234,14 +246,11 @@ async function attemptBuildCapture(
234
246
  setRequestContextParams(preview.params ?? {}, preview.routeKey);
235
247
  }
236
248
 
237
- const routeReverse = createReverseFunction(
238
- getGlobalRouteMap(),
239
- preview?.routeKey,
240
- preview?.params ?? {},
241
- preview?.routeKey
242
- ? isRouteRootScoped(preview.routeKey, router.id)
243
- : undefined,
244
- );
249
+ // Global-only, like the live request's middleware reverse
250
+ // (rsc/handler.ts: createReverseFunction(getRequiredRouteMap())): no
251
+ // include() scope, no param auto-fill. A scoped reverse here resolved
252
+ // `.name` at build while the live request threw `Unknown route`.
253
+ const middlewareReverse = createReverseFunction(getGlobalRouteMap());
245
254
 
246
255
  const runCapture = () =>
247
256
  runBuildCaptureFinal({
@@ -255,6 +264,9 @@ async function attemptBuildCapture(
255
264
  setMismatchedRouteName: (routeName) => {
256
265
  mismatchedRouteName = routeName;
257
266
  },
267
+ markTimedOut: () => {
268
+ timedOut = true;
269
+ },
258
270
  });
259
271
 
260
272
  const routeMiddleware =
@@ -268,7 +280,7 @@ async function attemptBuildCapture(
268
280
  env,
269
281
  variables,
270
282
  runCapture,
271
- routeReverse,
283
+ middlewareReverse,
272
284
  baseCtx,
273
285
  );
274
286
 
@@ -281,7 +293,7 @@ async function attemptBuildCapture(
281
293
  env,
282
294
  variables,
283
295
  runRouteMiddleware,
284
- routeReverse,
296
+ middlewareReverse,
285
297
  baseCtx,
286
298
  );
287
299
  });
@@ -294,6 +306,7 @@ async function attemptBuildCapture(
294
306
  if (outcome === "route-mismatch") {
295
307
  return { outcome, matchedRouteName: mismatchedRouteName };
296
308
  }
309
+ if (outcome === "no-shell" && timedOut) return { outcome, timedOut: true };
297
310
  return { outcome };
298
311
  }
299
312
 
@@ -313,6 +326,8 @@ interface BuildCaptureFinalOptions {
313
326
  router: any;
314
327
  url: URL;
315
328
  setMismatchedRouteName(routeName: string | undefined): void;
329
+ /** The attempt ran out of ppr.captureTimeout (BuildShellCaptureResult.timedOut). */
330
+ markTimedOut(): void;
316
331
  }
317
332
 
318
333
  async function runBuildMiddlewareEnvelope<TEnv>(
@@ -372,15 +387,28 @@ async function runBuildCaptureFinal(
372
387
  // invoking this when baseCtx._dynamic is set. A loader/handler opting out
373
388
  // DURING the capture render is caught by the derivedCtx._dynamic check below.
374
389
 
375
- const { derivedCtx, freshHandleStore } = deriveShellCaptureContext(baseCtx, {
390
+ const derivation = deriveShellCaptureContext(baseCtx, {
376
391
  ttl: opts.ttl,
377
392
  swr: opts.swr,
378
393
  });
394
+ const { derivedCtx, freshHandleStore } = derivation;
379
395
 
380
396
  const outcome = await runWithRequestContext(derivedCtx, async () => {
381
- let match;
397
+ // Same one deadline as the runtime capture: the match, the record-first
398
+ // step, then the prerender with what is left.
399
+ const deadline =
400
+ Date.now() + (opts.captureTimeout ?? SHELL_CAPTURE_MAX_WAIT_MS);
401
+ let match: MatchResult;
382
402
  try {
383
- match = await router.match(request, { env });
403
+ const matched = await withinDeadline<MatchResult>(
404
+ router.match(request, { env }),
405
+ deadline,
406
+ );
407
+ if (!matched.done) {
408
+ options.markTimedOut();
409
+ return "no-shell" as const;
410
+ }
411
+ match = matched.value;
384
412
  } catch (error) {
385
413
  if (isPlainPathMiss(error, opts.urlPath)) {
386
414
  return "route-mismatch" as const;
@@ -395,8 +423,25 @@ async function runBuildCaptureFinal(
395
423
 
396
424
  setRequestContextParams(match.params, match.routeName);
397
425
 
398
- const payload = buildFullPayload(
426
+ // Same record-first step as the runtime capture. A prerendered URL's
427
+ // match comes from the prerender store (no handler runs, no doc record),
428
+ // which settleCaptureRecord reports as `prerender`.
429
+ const settled = await settleCaptureRecord(
399
430
  match,
431
+ derivation,
432
+ descriptor,
433
+ deadline,
434
+ );
435
+ if (settled.kind === "refused") {
436
+ return derivedCtx._dynamic ? ("dynamic" as const) : ("refused" as const);
437
+ }
438
+ if (settled.kind === "timeout") {
439
+ options.markTimedOut();
440
+ return "no-shell" as const;
441
+ }
442
+
443
+ const payload = buildFullPayload(
444
+ settled.match,
400
445
  // buildFullPayload reads only ctx.router.* and ctx.version.
401
446
  { router, version: opts.buildVersion } as unknown as HandlerContext<any>,
402
447
  url,
@@ -419,9 +464,15 @@ async function runBuildCaptureFinal(
419
464
  rscStream,
420
465
  freshHandleStore,
421
466
  derivedCtx,
422
- descriptor,
467
+ { ...descriptor, captureTimeout: Math.max(1, deadline - Date.now()) },
423
468
  );
424
- return derivedCtx._dynamic ? "dynamic" : captureOutcome;
469
+ if (captureOutcome === "no-shell" && Date.now() >= deadline) {
470
+ options.markTimedOut();
471
+ }
472
+ if (derivedCtx._dynamic) return "dynamic";
473
+ // A route cache() record that ran out mid-capture: the in-place retry
474
+ // (captureShellForBuild) reads or renders a newer one.
475
+ return captureOutcome === "expired" ? "no-shell" : captureOutcome;
425
476
  });
426
477
 
427
478
  return {