@rangojs/router 0.17.0 → 0.19.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -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,10 +32,13 @@ import {
31
32
  import {
32
33
  deriveShellCaptureContext,
33
34
  captureAndStoreShell,
35
+ settleCaptureRecord,
34
36
  delay,
35
37
  SHELL_CAPTURE_RETRY_DELAY_MS,
36
38
  type ShellCaptureDescriptor,
37
39
  } from "../rsc/shell-capture.js";
40
+ import { SHELL_CAPTURE_MAX_WAIT_MS } from "../rsc/shell-capture-constants.js";
41
+ import { raceDeadline } from "../cache/background-task.js";
38
42
  import { buildFullPayload } from "../rsc/full-payload.js";
39
43
  import { buildRouteMiddlewareEntries } from "../rsc/helpers.js";
40
44
  import type { RscPayload, SSRModule } from "../rsc/types.js";
@@ -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`,
@@ -210,78 +221,81 @@ async function attemptBuildCapture(
210
221
  tags: opts.tags,
211
222
  captureTimeout: opts.captureTimeout,
212
223
  store: collector as any,
213
- debug: opts.debug,
214
224
  maxSnapshotBytes: opts.maxSnapshotBytes,
215
225
  };
216
226
 
217
- let mismatchedRouteName: string | undefined;
218
- const result = await runWithRequestContext(baseCtx, async () => {
219
- const preview =
220
- typeof router.previewMatch === "function"
221
- ? await router.previewMatch(request, { env })
222
- : undefined;
223
- // These preview-based mismatches exit before any middleware/envelope runs,
224
- // so nothing consumes a response — only result.outcome is read below.
225
- if (preview === null) {
226
- return { outcome: "route-mismatch" } as const;
227
- }
228
- if (preview?.routeKey && preview.routeKey !== opts.routeName) {
229
- mismatchedRouteName = preview.routeKey;
230
- return { outcome: "route-mismatch" } as const;
231
- }
227
+ const result = await runWithRequestContext(
228
+ baseCtx,
229
+ async (): Promise<
230
+ | BuildCaptureRunResult
231
+ | { outcome: "route-mismatch"; matchedRouteName?: string }
232
+ > => {
233
+ const preview =
234
+ typeof router.previewMatch === "function"
235
+ ? await router.previewMatch(request, { env })
236
+ : undefined;
237
+ // These preview-based mismatches exit before any middleware/envelope runs,
238
+ // so nothing consumes a response.
239
+ if (preview === null) {
240
+ return { outcome: "route-mismatch" };
241
+ }
242
+ if (preview?.routeKey && preview.routeKey !== opts.routeName) {
243
+ return {
244
+ outcome: "route-mismatch",
245
+ matchedRouteName: preview.routeKey,
246
+ };
247
+ }
232
248
 
233
- if (preview?.routeKey) {
234
- setRequestContextParams(preview.params ?? {}, preview.routeKey);
235
- }
249
+ if (preview?.routeKey) {
250
+ setRequestContextParams(preview.params ?? {}, preview.routeKey);
251
+ }
236
252
 
237
- // Global-only, like the live request's middleware reverse
238
- // (rsc/handler.ts: createReverseFunction(getRequiredRouteMap())): no
239
- // include() scope, no param auto-fill. A scoped reverse here resolved
240
- // `.name` at build while the live request threw `Unknown route`.
241
- const middlewareReverse = createReverseFunction(getGlobalRouteMap());
253
+ // Global-only, like the live request's middleware reverse
254
+ // (rsc/handler.ts: createReverseFunction(getRequiredRouteMap())): no
255
+ // include() scope, no param auto-fill. A scoped reverse here resolved
256
+ // `.name` at build while the live request threw `Unknown route`.
257
+ const middlewareReverse = createReverseFunction(getGlobalRouteMap());
242
258
 
243
- const runCapture = () =>
244
- runBuildCaptureFinal({
245
- baseCtx,
246
- descriptor,
247
- env,
248
- opts,
249
- request,
250
- router,
251
- url,
252
- setMismatchedRouteName: (routeName) => {
253
- mismatchedRouteName = routeName;
254
- },
255
- });
259
+ const runCapture = () =>
260
+ runBuildCaptureFinal({
261
+ baseCtx,
262
+ descriptor,
263
+ env,
264
+ opts,
265
+ request,
266
+ router,
267
+ url,
268
+ });
256
269
 
257
- const routeMiddleware =
258
- preview?.routeMiddleware && preview.routeMiddleware.length > 0
259
- ? buildRouteMiddlewareEntries(preview.routeMiddleware)
270
+ const routeMiddleware =
271
+ preview?.routeMiddleware && preview.routeMiddleware.length > 0
272
+ ? buildRouteMiddlewareEntries(preview.routeMiddleware)
273
+ : [];
274
+ const runRouteMiddleware = () =>
275
+ runBuildMiddlewareEnvelope(
276
+ routeMiddleware,
277
+ request,
278
+ env,
279
+ variables,
280
+ runCapture,
281
+ middlewareReverse,
282
+ baseCtx,
283
+ );
284
+
285
+ const globalMiddleware = Array.isArray(router.middleware)
286
+ ? matchMiddleware(url.pathname, router.middleware)
260
287
  : [];
261
- const runRouteMiddleware = () =>
262
- runBuildMiddlewareEnvelope(
263
- routeMiddleware,
288
+ return runBuildMiddlewareEnvelope(
289
+ globalMiddleware,
264
290
  request,
265
291
  env,
266
292
  variables,
267
- runCapture,
293
+ runRouteMiddleware,
268
294
  middlewareReverse,
269
295
  baseCtx,
270
296
  );
271
-
272
- const globalMiddleware = Array.isArray(router.middleware)
273
- ? matchMiddleware(url.pathname, router.middleware)
274
- : [];
275
- return runBuildMiddlewareEnvelope(
276
- globalMiddleware,
277
- request,
278
- env,
279
- variables,
280
- runRouteMiddleware,
281
- middlewareReverse,
282
- baseCtx,
283
- );
284
- });
297
+ },
298
+ );
285
299
 
286
300
  const outcome = result.outcome;
287
301
  if (outcome === "stored" && collected !== null) {
@@ -289,7 +303,10 @@ async function attemptBuildCapture(
289
303
  return { outcome, entry: hit.entry, tags: hit.tags };
290
304
  }
291
305
  if (outcome === "route-mismatch") {
292
- return { outcome, matchedRouteName: mismatchedRouteName };
306
+ return { outcome, matchedRouteName: result.matchedRouteName };
307
+ }
308
+ if (outcome === "no-shell" && "timedOut" in result && result.timedOut) {
309
+ return { outcome, timedOut: true };
293
310
  }
294
311
  return { outcome };
295
312
  }
@@ -299,6 +316,10 @@ type BuildShellCaptureOutcome = BuildShellCaptureResult["outcome"];
299
316
  interface BuildCaptureRunResult {
300
317
  outcome: BuildShellCaptureOutcome;
301
318
  response: Response;
319
+ /** On route-mismatch: what this router's match landed on. */
320
+ matchedRouteName?: string;
321
+ /** On no-shell: the attempt ran out of ppr.captureTimeout. */
322
+ timedOut?: true;
302
323
  }
303
324
 
304
325
  interface BuildCaptureFinalOptions {
@@ -309,7 +330,6 @@ interface BuildCaptureFinalOptions {
309
330
  request: Request;
310
331
  router: any;
311
332
  url: URL;
312
- setMismatchedRouteName(routeName: string | undefined): void;
313
333
  }
314
334
 
315
335
  async function runBuildMiddlewareEnvelope<TEnv>(
@@ -369,61 +389,104 @@ async function runBuildCaptureFinal(
369
389
  // invoking this when baseCtx._dynamic is set. A loader/handler opting out
370
390
  // DURING the capture render is caught by the derivedCtx._dynamic check below.
371
391
 
372
- const { derivedCtx, freshHandleStore } = deriveShellCaptureContext(baseCtx, {
392
+ const derivation = deriveShellCaptureContext(baseCtx, {
373
393
  ttl: opts.ttl,
374
394
  swr: opts.swr,
375
395
  });
396
+ const { derivedCtx, freshHandleStore } = derivation;
397
+ // The capture generation starts before the match, as at runtime
398
+ // (ShellCacheEntry.createdAt; the build-shell read-through compares tag
399
+ // invalidations against it).
400
+ const captureStartedAt = Date.now();
376
401
 
377
- const outcome = await runWithRequestContext(derivedCtx, async () => {
378
- let match;
379
- try {
380
- match = await router.match(request, { env });
381
- } catch (error) {
382
- if (isPlainPathMiss(error, opts.urlPath)) {
383
- return "route-mismatch" as const;
402
+ const result = await runWithRequestContext(
403
+ derivedCtx,
404
+ async (): Promise<Omit<BuildCaptureRunResult, "response">> => {
405
+ // Same one deadline as the runtime capture: the match, the record-first
406
+ // step, then the prerender with what is left.
407
+ const deadline =
408
+ Date.now() + (opts.captureTimeout ?? SHELL_CAPTURE_MAX_WAIT_MS);
409
+ let match: MatchResult;
410
+ try {
411
+ const matched = await raceDeadline<MatchResult>(
412
+ router.match(request, { env }),
413
+ deadline,
414
+ );
415
+ if (!matched.done) return { outcome: "no-shell", timedOut: true };
416
+ match = matched.value;
417
+ } catch (error) {
418
+ if (isPlainPathMiss(error, opts.urlPath)) {
419
+ return { outcome: "route-mismatch" };
420
+ }
421
+ throw error;
384
422
  }
385
- throw error;
386
- }
387
- if (match.routeName !== opts.routeName) {
388
- options.setMismatchedRouteName(match.routeName);
389
- return "route-mismatch" as const;
390
- }
391
- if (match.redirect) return "redirect" as const;
423
+ if (match.routeName !== opts.routeName) {
424
+ return { outcome: "route-mismatch", matchedRouteName: match.routeName };
425
+ }
426
+ if (match.redirect) return { outcome: "redirect" };
392
427
 
393
- setRequestContextParams(match.params, match.routeName);
428
+ setRequestContextParams(match.params, match.routeName);
394
429
 
395
- const payload = buildFullPayload(
396
- match,
397
- // buildFullPayload reads only ctx.router.* and ctx.version.
398
- { router, version: opts.buildVersion } as unknown as HandlerContext<any>,
399
- url,
400
- derivedCtx,
401
- freshHandleStore,
402
- );
403
- const rscStream = renderToReadableStream<RscPayload>(payload, {
404
- onError: (error: unknown) => {
405
- if (opts.debug) {
406
- console.warn(
407
- `[rango] shell capture render error for ${opts.urlPath}:`,
408
- error,
409
- );
410
- }
411
- },
412
- });
430
+ // Same record-first step as the runtime capture. A prerendered URL's
431
+ // match comes from the prerender store (no handler runs, no doc record),
432
+ // which settleCaptureRecord reports as `prerender`.
433
+ const settled = await settleCaptureRecord(
434
+ match,
435
+ derivation,
436
+ descriptor,
437
+ deadline,
438
+ );
439
+ if (settled.kind === "refused") {
440
+ return { outcome: derivedCtx._dynamic ? "dynamic" : "refused" };
441
+ }
442
+ if (settled.kind === "timeout") {
443
+ return { outcome: "no-shell", timedOut: true };
444
+ }
413
445
 
414
- const captureOutcome = await captureAndStoreShell(
415
- { captureShellHTML: opts.captureShellHTML } as SSRModule,
416
- rscStream,
417
- freshHandleStore,
418
- derivedCtx,
419
- descriptor,
420
- );
421
- return derivedCtx._dynamic ? "dynamic" : captureOutcome;
422
- });
446
+ const payload = buildFullPayload(
447
+ settled.match,
448
+ // buildFullPayload reads only ctx.router.* and ctx.version.
449
+ {
450
+ router,
451
+ version: opts.buildVersion,
452
+ } as unknown as HandlerContext<any>,
453
+ url,
454
+ derivedCtx,
455
+ freshHandleStore,
456
+ );
457
+ const rscStream = renderToReadableStream<RscPayload>(payload, {
458
+ onError: (error: unknown) => {
459
+ if (opts.debug) {
460
+ console.warn(
461
+ `[rango] shell capture render error for ${opts.urlPath}:`,
462
+ error,
463
+ );
464
+ }
465
+ },
466
+ });
467
+
468
+ const captureOutcome = await captureAndStoreShell(
469
+ { captureShellHTML: opts.captureShellHTML } as SSRModule,
470
+ rscStream,
471
+ derivedCtx,
472
+ { ...descriptor, captureTimeout: Math.max(1, deadline - Date.now()) },
473
+ captureStartedAt,
474
+ );
475
+ if (derivedCtx._dynamic) return { outcome: "dynamic" };
476
+ if (captureOutcome === "no-shell" && Date.now() >= deadline) {
477
+ return { outcome: "no-shell", timedOut: true };
478
+ }
479
+ // A route cache() record that ran out mid-capture: the in-place retry
480
+ // (captureShellForBuild) reads or renders a newer one.
481
+ return {
482
+ outcome: captureOutcome === "expired" ? "no-shell" : captureOutcome,
483
+ };
484
+ },
485
+ );
423
486
 
424
487
  return {
425
- outcome,
426
- response: responseForBuildCaptureOutcome(outcome),
488
+ ...result,
489
+ response: responseForBuildCaptureOutcome(result.outcome),
427
490
  };
428
491
  }
429
492
 
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { assertTransitionWhenShape } from "../transition-when-ref.js";
2
3
  import type {
3
4
  PartialCacheOptions,
4
5
  Handler,
@@ -909,12 +910,13 @@ const loader: RouteHelpers<any, any>["loader"] = (
909
910
  // resolved here, at DSL-evaluation time, for the same reason
910
911
  // loading({ ssr: false }) is (below) — per-isSSR entry caching makes the
911
912
  // flag request-mode-correct with no isSSR threading; see LoaderEntry.
913
+ // `bake` is the lane, the same on both evaluations.
912
914
  const loaderEntry: LoaderEntry = {
913
915
  loader: loaderDef,
914
916
  revalidate: [] as ShouldRevalidateFn<any, any>[],
915
- ...(optionsGiven?.ssr === false && ctx.isSSR
916
- ? { awaitBeforeFlush: true as const }
917
- : {}),
917
+ ...(optionsGiven?.ssr === false && { bake: true as const }),
918
+ ...(optionsGiven?.ssr === false &&
919
+ ctx.isSSR && { awaitBeforeFlush: true as const }),
918
920
  };
919
921
 
920
922
  // Merge handler.use defaults (attached to the loader definition) with explicit use
@@ -1006,6 +1008,7 @@ const transition = (
1006
1008
  const { store, ctx } = requireDslContext(
1007
1009
  "transition() must be called inside urls()",
1008
1010
  );
1011
+ assertTransitionWhenShape(config.when);
1009
1012
 
1010
1013
  // Allocate a single index for this transition() call (used in all paths),
1011
1014
  // mirroring cache() — the child form uses it for the name, the wrapper form
@@ -188,7 +188,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
188
188
  *
189
189
  * // Conditional activation via the config object's `when` selector
190
190
  * intercept("@modal", "card", <CardModal />, {
191
- * when: ({ from }) => from.pathname.startsWith("/board"),
191
+ * when: ({ from }) => from.url.pathname.startsWith("/board"),
192
192
  * })
193
193
  *
194
194
  * // Config + other use-items: config is arg 4, use is arg 5
@@ -196,7 +196,7 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
196
196
  * "@modal",
197
197
  * "card",
198
198
  * <CardModal />,
199
- * { when: ({ from }) => from.pathname.startsWith("/board") },
199
+ * { when: ({ from }) => from.url.pathname.startsWith("/board") },
200
200
  * () => [loader(CardDetailLoader)],
201
201
  * )
202
202
  * ```
@@ -501,12 +501,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
501
501
  * transition({}) is startTransition + ViewTransition under the default and
502
502
  * startTransition only when the router sets viewTransition: false.
503
503
  *
504
- * Conditional hold: pass `when: (ctx) => boolean` to gate the transition per
505
- * request. It normally runs server-side after the route handler. On a `ppr`
506
- * route it is automatically hoisted before route handlers and reevaluated on
507
- * every replay, so it may read URL/params/action/env and middleware-set context
508
- * but not handler-set context. Returning false drops this transition for the
509
- * request. This is distinct from intercept()'s match-time `when` selector.
504
+ * Conditional hold: pass `when` to gate the navigation in the browser,
505
+ * inline (the build hoists the literal into a client module) or as a
506
+ * "use client" export. It runs once per
507
+ * navigation with { kind, from, to, isAction, action }, never on the server;
508
+ * false makes the commit urgent (no hold, no view transition). This is
509
+ * distinct from intercept()'s match-time `when` selector.
510
510
  *
511
511
  * ```typescript
512
512
  * // Attach to a single route
@@ -525,15 +525,15 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
525
525
  * transition({ viewTransition: false }),
526
526
  * ])
527
527
  *
528
- * // Hold only when the handler decided to (post-handler predicate):
528
+ * // Hold only when arriving from the list (a browser predicate):
529
529
  * path("/product/:id", ProductPage, { name: "product" }, () => [
530
- * transition({ when: (ctx) => ctx.get(KeepScroll) === true }),
530
+ * transition({ when: ({ from }) => from.routeName === "products.list" }),
531
531
  * ])
532
532
  * ```
533
533
  * @param config - ViewTransition configuration (enter, exit, update, share,
534
534
  * default, name), `viewTransition: "auto" | false` to toggle the router
535
535
  * boundary (createRouter({ viewTransition }) sets the app-wide default), and
536
- * `when: (ctx) => boolean` to gate the transition per request
536
+ * `when` to gate the transition per navigation (a browser predicate)
537
537
  * @param children - Optional callback returning child routes to wrap
538
538
  */
539
539
  transition: {
package/src/route-name.ts CHANGED
@@ -32,6 +32,18 @@ export function isAutoGeneratedRouteName(name: string): boolean {
32
32
  });
33
33
  }
34
34
 
35
+ /**
36
+ * The route name user code may see: a user-defined name, or undefined for an
37
+ * internal one (unnamed path(), hidden include scope). Internal names are
38
+ * route keys, not API, and must not leak into transition({ when }) or
39
+ * intercept({ when }) locations.
40
+ */
41
+ export function publicRouteName(
42
+ routeKey: string | undefined,
43
+ ): string | undefined {
44
+ return routeKey && !isAutoGeneratedRouteName(routeKey) ? routeKey : undefined;
45
+ }
46
+
35
47
  /**
36
48
  * Validate that a user-provided route name does not collide with
37
49
  * reserved internal prefixes. Checks every dot-separated segment,
@@ -6,6 +6,7 @@
6
6
  * RSC routes and response routes (JSON, text, image, stream, etc.).
7
7
  */
8
8
 
9
+ import { requestHeaders } from "../server/request-headers.js";
9
10
  import type { EntryData } from "../server/context.js";
10
11
  import type { NegotiateVariant } from "../build/route-trie.js";
11
12
  import type { CollectedMiddleware } from "./middleware-types.js";
@@ -245,7 +246,9 @@ export async function negotiateRoute(
245
246
  return null;
246
247
  }
247
248
 
248
- const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
249
+ const acceptEntries = parseAcceptTypes(
250
+ requestHeaders(request).get("accept") || "",
251
+ );
249
252
 
250
253
  // Variants carry the variant's own pa (positional param names); the synthetic
251
254
  // primary/RSC candidates have none (their params are already keyed correctly).
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import { createElement, type ReactNode } from "react";
9
- import type { EntryData } from "../server/context";
9
+ import type { EntryData } from "../server/context.js";
10
+ import { runIdentityExempt } from "../cache/cache-exec-scope.js";
10
11
  import type {
11
12
  ResolvedSegment,
12
13
  ErrorInfo,
@@ -92,7 +93,9 @@ export function invokeOnError<TEnv = any>(
92
93
  };
93
94
 
94
95
  try {
95
- const result = onError(errorContext);
96
+ // An observer: a request header it logs is not rendered, so the identity
97
+ // guards let it read one inside a capture or a cached scope (#976).
98
+ const result = runIdentityExempt(() => onError(errorContext));
96
99
  // If onError returns a promise, catch any rejections
97
100
  if (result instanceof Promise) {
98
101
  result.catch((callbackError) => {
@@ -5,7 +5,10 @@
5
5
  */
6
6
 
7
7
  import type { HandlerContext, InternalHandlerContext } from "../types";
8
- import { _getRequestContext } from "../server/request-context.js";
8
+ import {
9
+ _getRequestContext,
10
+ type RequestContext,
11
+ } from "../server/request-context.js";
9
12
  import { getSearchSchema, isRouteRootScoped } from "../route-map-builder.js";
10
13
  import { parseSearchParams, serializeSearchParams } from "../search-params.js";
11
14
  import {
@@ -18,6 +21,10 @@ import {
18
21
  assertCachedHeaderWriteAllowed,
19
22
  assertNonCacheableReadAllowed,
20
23
  } from "../server/context.js";
24
+ import {
25
+ guardRequestHeaders,
26
+ readGuardedTheme,
27
+ } from "../server/cookie-store.js";
21
28
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
22
29
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
30
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
@@ -29,6 +36,31 @@ import { fireAndForgetWaitUntil } from "../types/request-scope.js";
29
36
  // once at module load instead of per createHandlerContext (per-request) call.
30
37
  const MUTATING_HEADERS_METHODS = new Set(["set", "append", "delete"]);
31
38
 
39
+ /** The request context a handler ctx's `theme` getter reads (THEME_DESCRIPTOR). */
40
+ const THEME_REQUEST_CONTEXT = Symbol("rango:theme-request-context");
41
+
42
+ /**
43
+ * ctx.theme is the visitor's theme cookie, guarded like cookies() (#971,
44
+ * readGuardedTheme): a getter, so creating the ctx reads nothing and
45
+ * only a handler that reads the theme refuses a ppr capture or throws on a
46
+ * cache() miss. Non-enumerable, so serializing the ctx does not read it:
47
+ * React's dev Flight debug info JSON-stringifies server component props and
48
+ * replayed console.log args, and an enumerable getter read there refused the
49
+ * capture of a route that never read the theme. The trade: a spread or
50
+ * Object.assign copy of the ctx does not carry `theme`. One shared getter
51
+ * with the request context on a symbol key: a getter closure per ctx gives
52
+ * every ctx its own property shape (V8 dictionary mode). The guard reads the
53
+ * ambient context, as cookies() does, and falls back to the ctx's own when
54
+ * there is none.
55
+ */
56
+ const THEME_DESCRIPTOR: PropertyDescriptor = {
57
+ get(this: { [THEME_REQUEST_CONTEXT]?: RequestContext }) {
58
+ return readGuardedTheme(this[THEME_REQUEST_CONTEXT], "ctx.theme");
59
+ },
60
+ enumerable: false,
61
+ configurable: true,
62
+ };
63
+
32
64
  /**
33
65
  * `headers` whose mutating methods run `assertWrite(method)` first. Shared by
34
66
  * the handler ctx (createHandlerContext) and the middleware ctx
@@ -263,7 +295,10 @@ export function createHandlerContext<TEnv>(
263
295
  requestContext?.dynamic();
264
296
  },
265
297
  dev: false,
266
- request,
298
+ // Usually the request context's (guarded at creation). A request the
299
+ // router builds and matches (the progressive-enhancement re-render, a
300
+ // navigation-only ppr capture) is guarded here, before any handler runs.
301
+ request: guardRequestHeaders(request),
267
302
  searchParams,
268
303
  search: searchSchema ? resolvedSearchParams : {},
269
304
  pathname,
@@ -279,7 +314,7 @@ export function createHandlerContext<TEnv>(
279
314
  // Read-time guard: non-cacheable var inside "use cache" or cache() →
280
315
  // throw. Works for both ContextVar tokens and string keys.
281
316
  if (isNonCacheable(variables, keyOrVar)) {
282
- assertNonCacheableReadAllowed(keyOrVar);
317
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
283
318
  }
284
319
  return contextGet(variables, keyOrVar);
285
320
  }) as HandlerContext<any, TEnv>["get"],
@@ -295,8 +330,7 @@ export function createHandlerContext<TEnv>(
295
330
  use: () => {
296
331
  throw new Error("ctx.use() called before loaders were initialized");
297
332
  },
298
- // Theme support (when enabled via router config)
299
- theme: requestContext?.theme,
333
+ // theme: THEME_DESCRIPTOR, defined after this literal.
300
334
  setTheme: requestContext?.setTheme,
301
335
  // Location state support (delegates to request context)
302
336
  setLocationState(entries) {
@@ -325,6 +359,9 @@ export function createHandlerContext<TEnv>(
325
359
  _responseType: responseType,
326
360
  _routeName: routeName,
327
361
  };
362
+ (ctx as { [THEME_REQUEST_CONTEXT]?: unknown })[THEME_REQUEST_CONTEXT] =
363
+ requestContext;
364
+ Object.defineProperty(ctx, "theme", THEME_DESCRIPTOR);
328
365
  // Brand with taint symbol so "use cache" excludes ctx from cache keys
329
366
  (ctx as any)[NOCACHE_SYMBOL] = true;
330
367
  return ctx;