@rangojs/router 0.18.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -33,12 +33,12 @@ import {
33
33
  deriveShellCaptureContext,
34
34
  captureAndStoreShell,
35
35
  settleCaptureRecord,
36
- withinDeadline,
37
36
  delay,
38
- SHELL_CAPTURE_MAX_WAIT_MS,
39
37
  SHELL_CAPTURE_RETRY_DELAY_MS,
40
38
  type ShellCaptureDescriptor,
41
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";
42
42
  import { buildFullPayload } from "../rsc/full-payload.js";
43
43
  import { buildRouteMiddlewareEntries } from "../rsc/helpers.js";
44
44
  import type { RscPayload, SSRModule } from "../rsc/types.js";
@@ -221,82 +221,81 @@ async function attemptBuildCapture(
221
221
  tags: opts.tags,
222
222
  captureTimeout: opts.captureTimeout,
223
223
  store: collector as any,
224
- debug: opts.debug,
225
224
  maxSnapshotBytes: opts.maxSnapshotBytes,
226
225
  };
227
226
 
228
- let mismatchedRouteName: string | undefined;
229
- let timedOut = false;
230
- const result = await runWithRequestContext(baseCtx, async () => {
231
- const preview =
232
- typeof router.previewMatch === "function"
233
- ? await router.previewMatch(request, { env })
234
- : undefined;
235
- // These preview-based mismatches exit before any middleware/envelope runs,
236
- // so nothing consumes a response — only result.outcome is read below.
237
- if (preview === null) {
238
- return { outcome: "route-mismatch" } as const;
239
- }
240
- if (preview?.routeKey && preview.routeKey !== opts.routeName) {
241
- mismatchedRouteName = preview.routeKey;
242
- return { outcome: "route-mismatch" } as const;
243
- }
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
+ }
244
248
 
245
- if (preview?.routeKey) {
246
- setRequestContextParams(preview.params ?? {}, preview.routeKey);
247
- }
249
+ if (preview?.routeKey) {
250
+ setRequestContextParams(preview.params ?? {}, preview.routeKey);
251
+ }
248
252
 
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());
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());
254
258
 
255
- const runCapture = () =>
256
- runBuildCaptureFinal({
257
- baseCtx,
258
- descriptor,
259
- env,
260
- opts,
261
- request,
262
- router,
263
- url,
264
- setMismatchedRouteName: (routeName) => {
265
- mismatchedRouteName = routeName;
266
- },
267
- markTimedOut: () => {
268
- timedOut = true;
269
- },
270
- });
259
+ const runCapture = () =>
260
+ runBuildCaptureFinal({
261
+ baseCtx,
262
+ descriptor,
263
+ env,
264
+ opts,
265
+ request,
266
+ router,
267
+ url,
268
+ });
269
+
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
+ );
271
284
 
272
- const routeMiddleware =
273
- preview?.routeMiddleware && preview.routeMiddleware.length > 0
274
- ? buildRouteMiddlewareEntries(preview.routeMiddleware)
285
+ const globalMiddleware = Array.isArray(router.middleware)
286
+ ? matchMiddleware(url.pathname, router.middleware)
275
287
  : [];
276
- const runRouteMiddleware = () =>
277
- runBuildMiddlewareEnvelope(
278
- routeMiddleware,
288
+ return runBuildMiddlewareEnvelope(
289
+ globalMiddleware,
279
290
  request,
280
291
  env,
281
292
  variables,
282
- runCapture,
293
+ runRouteMiddleware,
283
294
  middlewareReverse,
284
295
  baseCtx,
285
296
  );
286
-
287
- const globalMiddleware = Array.isArray(router.middleware)
288
- ? matchMiddleware(url.pathname, router.middleware)
289
- : [];
290
- return runBuildMiddlewareEnvelope(
291
- globalMiddleware,
292
- request,
293
- env,
294
- variables,
295
- runRouteMiddleware,
296
- middlewareReverse,
297
- baseCtx,
298
- );
299
- });
297
+ },
298
+ );
300
299
 
301
300
  const outcome = result.outcome;
302
301
  if (outcome === "stored" && collected !== null) {
@@ -304,9 +303,11 @@ async function attemptBuildCapture(
304
303
  return { outcome, entry: hit.entry, tags: hit.tags };
305
304
  }
306
305
  if (outcome === "route-mismatch") {
307
- 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 };
308
310
  }
309
- if (outcome === "no-shell" && timedOut) return { outcome, timedOut: true };
310
311
  return { outcome };
311
312
  }
312
313
 
@@ -315,6 +316,10 @@ type BuildShellCaptureOutcome = BuildShellCaptureResult["outcome"];
315
316
  interface BuildCaptureRunResult {
316
317
  outcome: BuildShellCaptureOutcome;
317
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;
318
323
  }
319
324
 
320
325
  interface BuildCaptureFinalOptions {
@@ -325,9 +330,6 @@ interface BuildCaptureFinalOptions {
325
330
  request: Request;
326
331
  router: any;
327
332
  url: URL;
328
- setMismatchedRouteName(routeName: string | undefined): void;
329
- /** The attempt ran out of ppr.captureTimeout (BuildShellCaptureResult.timedOut). */
330
- markTimedOut(): void;
331
333
  }
332
334
 
333
335
  async function runBuildMiddlewareEnvelope<TEnv>(
@@ -392,92 +394,99 @@ async function runBuildCaptureFinal(
392
394
  swr: opts.swr,
393
395
  });
394
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();
395
401
 
396
- const outcome = await runWithRequestContext(derivedCtx, async () => {
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;
402
- try {
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;
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;
410
422
  }
411
- match = matched.value;
412
- } catch (error) {
413
- if (isPlainPathMiss(error, opts.urlPath)) {
414
- return "route-mismatch" as const;
423
+ if (match.routeName !== opts.routeName) {
424
+ return { outcome: "route-mismatch", matchedRouteName: match.routeName };
415
425
  }
416
- throw error;
417
- }
418
- if (match.routeName !== opts.routeName) {
419
- options.setMismatchedRouteName(match.routeName);
420
- return "route-mismatch" as const;
421
- }
422
- if (match.redirect) return "redirect" as const;
426
+ if (match.redirect) return { outcome: "redirect" };
423
427
 
424
- setRequestContextParams(match.params, match.routeName);
428
+ setRequestContextParams(match.params, match.routeName);
425
429
 
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(
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
- }
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
+ }
442
445
 
443
- const payload = buildFullPayload(
444
- settled.match,
445
- // buildFullPayload reads only ctx.router.* and ctx.version.
446
- { router, version: opts.buildVersion } as unknown as HandlerContext<any>,
447
- url,
448
- derivedCtx,
449
- freshHandleStore,
450
- );
451
- const rscStream = renderToReadableStream<RscPayload>(payload, {
452
- onError: (error: unknown) => {
453
- if (opts.debug) {
454
- console.warn(
455
- `[rango] shell capture render error for ${opts.urlPath}:`,
456
- error,
457
- );
458
- }
459
- },
460
- });
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
+ });
461
467
 
462
- const captureOutcome = await captureAndStoreShell(
463
- { captureShellHTML: opts.captureShellHTML } as SSRModule,
464
- rscStream,
465
- freshHandleStore,
466
- derivedCtx,
467
- { ...descriptor, captureTimeout: Math.max(1, deadline - Date.now()) },
468
- );
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;
476
- });
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
+ );
477
486
 
478
487
  return {
479
- outcome,
480
- response: responseForBuildCaptureOutcome(outcome),
488
+ ...result,
489
+ response: responseForBuildCaptureOutcome(result.outcome),
481
490
  };
482
491
  }
483
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,
@@ -1007,6 +1008,7 @@ const transition = (
1007
1008
  const { store, ctx } = requireDslContext(
1008
1009
  "transition() must be called inside urls()",
1009
1010
  );
1011
+ assertTransitionWhenShape(config.when);
1010
1012
 
1011
1013
  // Allocate a single index for this transition() call (used in all paths),
1012
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) => {
@@ -21,7 +21,10 @@ import {
21
21
  assertCachedHeaderWriteAllowed,
22
22
  assertNonCacheableReadAllowed,
23
23
  } from "../server/context.js";
24
- import { assertThemeReadAllowed } from "../server/cookie-store.js";
24
+ import {
25
+ guardRequestHeaders,
26
+ readGuardedTheme,
27
+ } from "../server/cookie-store.js";
25
28
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
26
29
  import { isAutoGeneratedRouteName } from "../route-name.js";
27
30
  import { PRERENDER_PASSTHROUGH } from "../prerender.js";
@@ -38,7 +41,7 @@ const THEME_REQUEST_CONTEXT = Symbol("rango:theme-request-context");
38
41
 
39
42
  /**
40
43
  * ctx.theme is the visitor's theme cookie, guarded like cookies() (#971,
41
- * assertThemeReadAllowed): a getter, so creating the ctx reads nothing and
44
+ * readGuardedTheme): a getter, so creating the ctx reads nothing and
42
45
  * only a handler that reads the theme refuses a ppr capture or throws on a
43
46
  * cache() miss. Non-enumerable, so serializing the ctx does not read it:
44
47
  * React's dev Flight debug info JSON-stringifies server component props and
@@ -52,10 +55,7 @@ const THEME_REQUEST_CONTEXT = Symbol("rango:theme-request-context");
52
55
  */
53
56
  const THEME_DESCRIPTOR: PropertyDescriptor = {
54
57
  get(this: { [THEME_REQUEST_CONTEXT]?: RequestContext }) {
55
- const requestContext = this[THEME_REQUEST_CONTEXT];
56
- if (!requestContext?._themeConfig) return undefined;
57
- assertThemeReadAllowed(_getRequestContext() ?? requestContext, "ctx.theme");
58
- return requestContext._readTheme();
58
+ return readGuardedTheme(this[THEME_REQUEST_CONTEXT], "ctx.theme");
59
59
  },
60
60
  enumerable: false,
61
61
  configurable: true,
@@ -295,7 +295,10 @@ export function createHandlerContext<TEnv>(
295
295
  requestContext?.dynamic();
296
296
  },
297
297
  dev: false,
298
- 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),
299
302
  searchParams,
300
303
  search: searchSchema ? resolvedSearchParams : {},
301
304
  pathname,
@@ -64,6 +64,8 @@ function latchPprHeaderScopeForInterceptTarget(
64
64
  * Check if an intercept's when conditions are satisfied.
65
65
  * All when() functions must return true for the intercept to activate.
66
66
  * If no when() conditions are defined, the intercept always activates.
67
+ * A selector that throws yields the conservative default, no intercept (the
68
+ * full page renders), and is logged; it never fails the request.
67
69
  *
68
70
  * During action revalidation, when() is NOT evaluated.
69
71
  */
@@ -84,7 +86,15 @@ export function evaluateInterceptWhen(
84
86
  return false;
85
87
  }
86
88
 
87
- return intercept.when.every((fn) => fn(selectorContext));
89
+ try {
90
+ return intercept.when.every((fn) => fn(selectorContext));
91
+ } catch (error) {
92
+ console.error(
93
+ `[rango] intercept({ when }) for route "${selectorContext.to.routeName ?? intercept.routeName}" (${intercept.slotName}) threw; rendering the full page without the intercept.`,
94
+ error,
95
+ );
96
+ return false;
97
+ }
88
98
  }
89
99
 
90
100
  /**
@@ -14,9 +14,11 @@ import {
14
14
  linkRecordedTags,
15
15
  linkValueTags,
16
16
  loaderTagSetsArmed,
17
+ markTagSetStart,
17
18
  readValueTags,
18
19
  tagLoaderValue,
19
20
  } from "../cache/cache-tag.js";
21
+ import { executionStart } from "../cache/tag-invalidation.js";
20
22
  import type {
21
23
  ResolvedSegment,
22
24
  HandlerContext,
@@ -396,6 +398,12 @@ function createLoaderExecutor<TEnv>(
396
398
  * payload's handle-stream lifetime.
397
399
  */
398
400
  silentHandles?: boolean;
401
+ /**
402
+ * A stale refresh's executor (setupLoaderAccess _runLoaderIsolated): its
403
+ * runs' pushes are diverted from the page's store, so a run's end says
404
+ * nothing about the page's replayed values (HandleStore.settleLoaderRun).
405
+ */
406
+ detached?: boolean;
399
407
  /**
400
408
  * loaderId -> the cache() tags of that loader's binding in this request
401
409
  * (setupLoaderAccess _bindLoaderCacheTags): an execution of it outside
@@ -410,6 +418,8 @@ function createLoaderExecutor<TEnv>(
410
418
  // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
411
419
  const reqCtxRef = _getRequestContext();
412
420
  const silentHandles = executorOptions?.silentHandles === true;
421
+ // A run's end reports to the page's store only when its pushes reach it.
422
+ const reportsRunEnd = !silentHandles && executorOptions?.detached !== true;
413
423
  const internal = ctx as InternalHandlerContext<any, TEnv>;
414
424
 
415
425
  // Dependency graph: loaderId -> set of loader IDs it directly depends on.
@@ -712,10 +722,17 @@ function createLoaderExecutor<TEnv>(
712
722
  //
713
723
  // The CONSUMPTION-LANE RULE: a handler-consumed loader's value is a
714
724
  // BAKED copy in every shared artifact (cache(), "use cache", the PPR
715
- // shell). The cache-purity guards permit its identity reads; the PPR
716
- // capture guard does not (server/context.ts assertNotInsideShellCapture).
725
+ // shell). The cache() guard permits its identity reads; a "use cache"
726
+ // body it was entered in and a PPR capture do not (server/context.ts
727
+ // guardIdentityRead).
717
728
 
718
729
  const recordedTags = recordTags ? new Set<string>() : undefined;
730
+ // A loader-cache write that stores or reads this value gates on the
731
+ // earliest start behind it (#977, earliestRecordedStart).
732
+ if (recordedTags) markTagSetStart(recordedTags, executionStart());
733
+ const runEndStore = reportsRunEnd
734
+ ? (reqCtxRef ?? _getRequestContext())?._handleStore
735
+ : undefined;
719
736
  const promise = observePhase(PHASES.loader(loader.$$id), () =>
720
737
  Promise.resolve(
721
738
  runInsideLoaderBodyScope(
@@ -725,6 +742,9 @@ function createLoaderExecutor<TEnv>(
725
742
  ),
726
743
  ).finally(() => {
727
744
  pendingLoaders.delete(loader.$$id);
745
+ // Its pushes are made: values replayed for it that none replaced
746
+ // go (a hole that throws or skips its push shows none).
747
+ runEndStore?.settleLoaderRun(loader.$$id);
728
748
  }),
729
749
  );
730
750
  if (recordedTags) {
@@ -809,7 +829,12 @@ export function setupLoaderAccess<TEnv>(
809
829
  _loaderCacheOverrides: { value: undefined },
810
830
  }),
811
831
  new Map(),
812
- executor,
832
+ {
833
+ get bindingTags() {
834
+ return executor.bindingTags;
835
+ },
836
+ detached: true,
837
+ },
813
838
  )(loader, null);
814
839
 
815
840
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {