@rangojs/router 0.23.0 → 0.24.1

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 (132) hide show
  1. package/dist/bin/rango.js +4 -3
  2. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  3. package/dist/types/browser/partial-update.d.ts +6 -4
  4. package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
  5. package/dist/types/browser/react/context.d.ts +3 -2
  6. package/dist/types/browser/types.d.ts +5 -1
  7. package/dist/types/build/route-types/scan-filter.d.ts +7 -1
  8. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  9. package/dist/types/cache/document-cache.d.ts +2 -10
  10. package/dist/types/client-urls/types.d.ts +8 -0
  11. package/dist/types/context-var.d.ts +12 -0
  12. package/dist/types/errors.d.ts +10 -0
  13. package/dist/types/handles/pushed-by.d.ts +8 -0
  14. package/dist/types/index.d.ts +4 -0
  15. package/dist/types/index.rsc.d.ts +2 -0
  16. package/dist/types/response-utils.d.ts +7 -0
  17. package/dist/types/reverse.d.ts +8 -8
  18. package/dist/types/route-definition/rewrite.d.ts +56 -0
  19. package/dist/types/route-map-builder.d.ts +7 -2
  20. package/dist/types/router/basename.d.ts +7 -0
  21. package/dist/types/router/handler-context.d.ts +1 -5
  22. package/dist/types/router/internal-params.d.ts +8 -0
  23. package/dist/types/router/middleware-types.d.ts +15 -0
  24. package/dist/types/router/revalidation.d.ts +1 -2
  25. package/dist/types/router/route-trie-builder.d.ts +10 -0
  26. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  27. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
  28. package/dist/types/rsc/full-payload.d.ts +11 -0
  29. package/dist/types/rsc/handler.d.ts +33 -0
  30. package/dist/types/rsc/helpers.d.ts +8 -0
  31. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  32. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  33. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  34. package/dist/types/server/request-context.d.ts +39 -2
  35. package/dist/types/server/rewrite-trail.d.ts +49 -0
  36. package/dist/types/server/slot-accessors.d.ts +29 -0
  37. package/dist/types/testing/index.d.ts +1 -1
  38. package/dist/types/testing/render-route.d.ts +8 -0
  39. package/dist/types/testing/run-middleware.d.ts +20 -0
  40. package/dist/types/testing/serve-shell-request.d.ts +2 -1
  41. package/dist/types/thenable-status.d.ts +6 -0
  42. package/dist/types/types/request-scope.d.ts +7 -0
  43. package/dist/types/vite/discovery/state.d.ts +12 -9
  44. package/dist/vite/index.js +164 -103
  45. package/package.json +1 -1
  46. package/skills/caching/SKILL.md +12 -0
  47. package/skills/catalog.json +1 -1
  48. package/skills/client-urls/SKILL.md +86 -1
  49. package/skills/hooks/SKILL.md +11 -6
  50. package/skills/hooks/data.md +3 -1
  51. package/skills/loader/SKILL.md +7 -1
  52. package/skills/middleware/SKILL.md +183 -2
  53. package/skills/ppr/SKILL.md +9 -0
  54. package/skills/rango/SKILL.md +1 -1
  55. package/skills/server-actions/SKILL.md +27 -0
  56. package/skills/testing/client-components.md +1 -0
  57. package/skills/testing/middleware.md +31 -10
  58. package/skills/testing/response-routes.md +1 -0
  59. package/skills/testing/serve-shell-request.md +19 -1
  60. package/skills/use-cache/SKILL.md +4 -2
  61. package/src/__augment-tests__/augmented.check.ts +20 -0
  62. package/src/browser/event-controller.ts +3 -0
  63. package/src/browser/merge-segment-loaders.ts +31 -18
  64. package/src/browser/navigation-bridge.ts +19 -3
  65. package/src/browser/partial-update.ts +27 -12
  66. package/src/browser/react/Link.tsx +7 -9
  67. package/src/browser/react/NavigationProvider.tsx +46 -26
  68. package/src/browser/react/context.ts +3 -2
  69. package/src/browser/react/use-router.ts +2 -4
  70. package/src/browser/server-action-bridge.ts +6 -0
  71. package/src/browser/types.ts +5 -1
  72. package/src/build/route-types/scan-filter.ts +11 -4
  73. package/src/cache/document-cache-policy.ts +131 -0
  74. package/src/cache/document-cache.ts +12 -119
  75. package/src/client-urls/client-root.tsx +3 -3
  76. package/src/client-urls/client-urls.ts +8 -1
  77. package/src/client-urls/types.ts +8 -0
  78. package/src/context-var.ts +32 -2
  79. package/src/errors.ts +19 -0
  80. package/src/handles/pushed-by.ts +26 -0
  81. package/src/index.rsc.ts +8 -0
  82. package/src/index.ts +7 -0
  83. package/src/response-utils.ts +19 -0
  84. package/src/reverse.ts +17 -14
  85. package/src/route-definition/redirect.ts +2 -15
  86. package/src/route-definition/rewrite.ts +146 -0
  87. package/src/route-map-builder.ts +14 -11
  88. package/src/router/basename.ts +20 -0
  89. package/src/router/handler-context.ts +3 -13
  90. package/src/router/intercept-resolution.ts +29 -12
  91. package/src/router/internal-params.ts +16 -0
  92. package/src/router/loader-resolution.ts +2 -0
  93. package/src/router/match-middleware/background-revalidation.ts +11 -9
  94. package/src/router/match-middleware/cache-lookup.ts +20 -15
  95. package/src/router/match-middleware/cache-store.ts +9 -11
  96. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  97. package/src/router/middleware-types.ts +16 -0
  98. package/src/router/middleware.ts +127 -49
  99. package/src/router/revalidation.ts +1 -1
  100. package/src/router/route-trie-builder.ts +28 -1
  101. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  102. package/src/router/segment-resolution/mask-nested.ts +8 -4
  103. package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
  104. package/src/router/segment-resolution/revalidation.ts +57 -33
  105. package/src/rsc/full-payload.ts +37 -5
  106. package/src/rsc/handler.ts +82 -55
  107. package/src/rsc/helpers.ts +24 -0
  108. package/src/rsc/loader-fetch.ts +79 -24
  109. package/src/rsc/response-cache-serve.ts +18 -2
  110. package/src/rsc/response-route-handler.ts +4 -7
  111. package/src/rsc/rewrite-passes.ts +203 -0
  112. package/src/rsc/rsc-rendering.ts +4 -2
  113. package/src/rsc/ssr-setup.ts +11 -5
  114. package/src/server/cookie-parse.ts +36 -12
  115. package/src/server/cookie-store.ts +7 -3
  116. package/src/server/handle-store.ts +48 -17
  117. package/src/server/request-context.ts +154 -63
  118. package/src/server/request-headers.ts +4 -14
  119. package/src/server/rewrite-trail.ts +190 -0
  120. package/src/server/slot-accessors.ts +57 -0
  121. package/src/ssr/index.tsx +118 -16
  122. package/src/testing/dispatch.ts +138 -67
  123. package/src/testing/index.ts +1 -0
  124. package/src/testing/render-route.tsx +16 -3
  125. package/src/testing/run-loader.ts +2 -0
  126. package/src/testing/run-middleware.ts +56 -11
  127. package/src/testing/serve-shell-request.ts +15 -2
  128. package/src/thenable-status.ts +11 -0
  129. package/src/types/request-scope.ts +8 -0
  130. package/src/vite/discovery/state.ts +21 -13
  131. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  132. package/src/vite/router-discovery.ts +29 -27
@@ -153,6 +153,31 @@ function sortSuffixParams(node: TrieNode): void {
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Give every static-children map in the trie a null prototype, in place, and
158
+ * return the trie (#1109). The walker indexes `node.s` with raw URL segments,
159
+ * so a map with Object.prototype would answer `/constructor` or `/__proto__`
160
+ * with an inherited member. The builder creates null-prototype maps; a trie
161
+ * that arrives as JSON (the build manifest) or across the dev runner boundary
162
+ * gets them here. Object.setPrototypeOf keeps a JSON-parsed map's fast
163
+ * properties, where Object.create(null) starts in dictionary mode.
164
+ */
165
+ export function nullPrototypeStaticChildren(node: TrieNode): TrieNode {
166
+ if (node.s) {
167
+ Object.setPrototypeOf(node.s, null);
168
+ for (const child of Object.values(node.s)) {
169
+ nullPrototypeStaticChildren(child);
170
+ }
171
+ }
172
+ if (node.xp) {
173
+ for (const child of Object.values(node.xp)) {
174
+ nullPrototypeStaticChildren(child.c);
175
+ }
176
+ }
177
+ if (node.p) nullPrototypeStaticChildren(node.p.c);
178
+ return node;
179
+ }
180
+
156
181
  /**
157
182
  * Insert a route into the trie. Optional params expand into two branches at
158
183
  * registration time (skip-first, then present), so each terminal lives at the
@@ -271,7 +296,9 @@ function insertSegments(
271
296
  const segment = segments[index];
272
297
 
273
298
  if (segment.type === "static") {
274
- if (!node.s) node.s = {};
299
+ // Null prototype: a segment named `constructor` or `__proto__` is an own
300
+ // key, never an inherited member to descend into and write a leaf on.
301
+ if (!node.s) node.s = Object.create(null) as Record<string, TrieNode>;
275
302
  if (!node.s[segment.value]) node.s[segment.value] = {};
276
303
  insertSegments(
277
304
  node.s[segment.value],
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { isValidElement, type ReactElement } from "react";
28
28
  import { isThenable } from "../../handles/is-thenable.js";
29
+ import { assignOwn } from "../../context-var.js";
29
30
  import {
30
31
  cloneElementWithProps,
31
32
  isPlainDataObject,
@@ -178,7 +179,7 @@ async function elideNested(
178
179
  for (const key of Object.keys(source)) {
179
180
  const r = await elideNested(source[key], seen);
180
181
  if (r.state === "rejected") return r;
181
- out[key] = r.value;
182
+ assignOwn(out, key, r.value);
182
183
  if (r.value !== source[key]) changed = true;
183
184
  result.hasHole ||= r.hasHole;
184
185
  }
@@ -301,13 +302,21 @@ export function overlayLoaderContainer(
301
302
  // Shape drift (fresh not a plain object): markers fall back to undefined.
302
303
  const freshObj = isPlainDataObject(fresh) ? fresh : undefined;
303
304
  let out: Record<string, unknown> | undefined;
305
+ // Own keys only: a key named like an Object.prototype member is data.
306
+ // The spread copies an own `__proto__` as data, so assigning a recorded
307
+ // key writes it; a fresh-only key goes through assignOwn.
304
308
  for (const key of Object.keys(recorded)) {
305
- const next = overlayLoaderContainer(freshObj?.[key], recorded[key]);
309
+ const next = overlayLoaderContainer(
310
+ freshObj && Object.hasOwn(freshObj, key) ? freshObj[key] : undefined,
311
+ recorded[key],
312
+ );
306
313
  if (next !== recorded[key]) (out ??= { ...recorded })[key] = next;
307
314
  }
308
315
  if (freshObj) {
309
316
  for (const key of Object.keys(freshObj)) {
310
- if (!(key in recorded)) (out ??= { ...recorded })[key] = freshObj[key];
317
+ if (!Object.hasOwn(recorded, key)) {
318
+ assignOwn((out ??= { ...recorded }), key, freshObj[key]);
319
+ }
311
320
  }
312
321
  }
313
322
  return out ?? recorded;
@@ -341,7 +350,12 @@ function overlayProps(
341
350
  const props = recorded.props;
342
351
  let changed: Record<string, unknown> | undefined;
343
352
  for (const key of Object.keys(props)) {
344
- const next = overlayLoaderContainer(freshProps?.[key], props[key]);
353
+ const next = overlayLoaderContainer(
354
+ freshProps && Object.hasOwn(freshProps, key)
355
+ ? freshProps[key]
356
+ : undefined,
357
+ props[key],
358
+ );
345
359
  if (next !== props[key]) (changed ??= {})[key] = next;
346
360
  }
347
361
  return changed ? cloneElementWithProps(recorded, changed) : recorded;
@@ -8,10 +8,12 @@
8
8
  *
9
9
  * Deliberately a LEAF module: loader-mask (the other natural home) imports
10
10
  * request-context, so any funnel importing the mask from there inherits that
11
- * graph. This module imports only is-thenable and react.
11
+ * graph. This module imports only react and two leaves, is-thenable and
12
+ * context-var.
12
13
  */
13
14
 
14
15
  import { cloneElement, isValidElement, type ReactElement } from "react";
16
+ import { assignOwn } from "../../context-var.js";
15
17
  import { isThenable } from "../../handles/is-thenable.js";
16
18
 
17
19
  /**
@@ -52,7 +54,8 @@ type KeyValidated = { _store?: { validated?: unknown } };
52
54
  * keyless child of a static children list (validated at creation) would warn
53
55
  * "Each child in a list should have a unique key" once cloned. Pass only the
54
56
  * props that change: cloneElement re-parents the element (owner) when its
55
- * config carries `ref`.
57
+ * config carries `ref`. cloneElement copies props by assignment, so a prop
58
+ * key named `__proto__` can't survive a clone; prop copies don't use assignOwn.
56
59
  */
57
60
  export function cloneElementWithProps(
58
61
  element: ReactElement,
@@ -206,8 +209,9 @@ export function maskNestedContainerThenables(
206
209
  seen.set(value, out);
207
210
  let changed = false;
208
211
  for (const key of Object.keys(source)) {
209
- out[key] = maskNestedContainerThenables(source[key], seen, report);
210
- if (out[key] !== source[key]) changed = true;
212
+ const masked = maskNestedContainerThenables(source[key], seen, report);
213
+ assignOwn(out, key, masked);
214
+ if (masked !== source[key]) changed = true;
211
215
  }
212
216
  if (changed) return out;
213
217
  seen.set(value, value);
@@ -99,11 +99,8 @@ export interface PrefetchDeferral {
99
99
  * not stored and not held. Empty outside a prefetch.
100
100
  */
101
101
  readonly skipped: ReadonlySet<string>;
102
- /**
103
- * One of `skipped` is a chain entry: nothing below it runs, so the response
104
- * has no content for it, a route cache() record included.
105
- */
106
- readonly skipsChain: boolean;
102
+ /** How this request may use the route's cache() record (recordUse). */
103
+ readonly record: RecordUse;
107
104
  /** Segment id of the handler unit this prefetch skipped. */
108
105
  deferredUnit?: string;
109
106
  /**
@@ -125,6 +122,31 @@ export interface PrefetchDeferral {
125
122
  deferredLoaders?: Map<string, DeferredLoader>;
126
123
  }
127
124
 
125
+ /**
126
+ * How a request may use the route's cache() record, decided once per match
127
+ * (planPrefetchDeferral) for the cache middleware.
128
+ */
129
+ export interface RecordUse {
130
+ /**
131
+ * withCacheLookup reads it. False when this prefetch skips a chain unit
132
+ * (PrefetchDeferral.skipped): the unit takes everything below it, so the
133
+ * read would replay the record's handle pushes for segments the response
134
+ * does not carry. Decided from what the client holds, never per tree: a
135
+ * prefetch that holds the flagged layout reads and writes the record like
136
+ * any other request.
137
+ */
138
+ readonly read: boolean;
139
+ /** withCacheStore writes it. False where `read` is: the record would be incomplete. */
140
+ readonly write: boolean;
141
+ /**
142
+ * A background re-render of the whole route may write it: the stale-hit
143
+ * refresh (withBackgroundRevalidation) and the proactive write a partial
144
+ * response needs (withCacheStore). False for a fill: the re-render runs
145
+ * the handlers of segments the client holds, which a fill does not run.
146
+ */
147
+ readonly refresh: boolean;
148
+ }
149
+
128
150
  /** A loader this prefetch deferred: PrefetchDeferral.deferredLoaders. */
129
151
  export interface DeferredLoader {
130
152
  /**
@@ -329,7 +351,12 @@ export function planPrefetchDeferral(
329
351
  }
330
352
  }
331
353
  }
332
- return { scope, mode, storedFrom, held, skipped, skipsChain };
354
+ const record: RecordUse = {
355
+ read: !skipsChain,
356
+ write: !skipsChain,
357
+ refresh: mode !== "fill",
358
+ };
359
+ return { scope, mode, storedFrom, held, skipped, record };
333
360
  }
334
361
 
335
362
  export function getPrefetchDeferral(
@@ -344,6 +371,30 @@ export function isFillRequest(ctx: HandlerContext<any, any>): boolean {
344
371
  return getPrefetchDeferral(ctx)?.mode === "fill";
345
372
  }
346
373
 
374
+ /**
375
+ * A fill's whole revalidation rule (R5): send a segment the client does not
376
+ * hold, keep one it holds, and consult nothing else (no revalidate()
377
+ * predicate, no params or search comparison, no trace entry). Undefined on
378
+ * any other request, where revalidation decides. `held` is the set the
379
+ * deciding site resolves with.
380
+ */
381
+ export function fillSends(
382
+ ctx: HandlerContext<any, any>,
383
+ segmentId: string,
384
+ held: ReadonlySet<string>,
385
+ ): boolean | undefined {
386
+ return isFillRequest(ctx) ? !held.has(segmentId) : undefined;
387
+ }
388
+
389
+ const RECORD_OPEN: RecordUse = { read: true, write: true, refresh: true };
390
+
391
+ /** PrefetchDeferral.record, or all of it open when the match has no plan. */
392
+ export function recordUse(
393
+ ctx: HandlerContext<any, any> | undefined,
394
+ ): RecordUse {
395
+ return getPrefetchDeferral(ctx)?.record ?? RECORD_OPEN;
396
+ }
397
+
347
398
  /**
348
399
  * Register a loader this prefetch deferred, with the segment that registers
349
400
  * it and how to start it there (PrefetchDeferral.deferredLoaders). The first
@@ -481,17 +532,6 @@ export function firstUnitCandidate(
481
532
  return undefined;
482
533
  }
483
534
 
484
- /**
485
- * This prefetch skips a layout above the route cache's boundary, and with it
486
- * everything the record below holds: withCacheLookup and withCacheStore leave
487
- * the record alone for it. Decided per request, from what the client holds:
488
- * a prefetch of the same tree that holds the layout reads and writes the
489
- * record like any other request.
490
- */
491
- export function defersAboveRecord(ctx: HandlerContext<any, any>): boolean {
492
- return getPrefetchDeferral(ctx)?.skipsChain === true;
493
- }
494
-
495
535
  /**
496
536
  * Record the handler unit this prefetch skipped. A `chain` entry takes what
497
537
  * the walk reaches after it (PrefetchDeferral.below); a slot is skipped
@@ -9,7 +9,10 @@
9
9
  import type { ReactNode } from "react";
10
10
  import { invariant } from "../../errors";
11
11
  import { revalidate } from "../loader-resolution.js";
12
- import { evaluateRevalidation } from "../revalidation.js";
12
+ import {
13
+ evaluateRevalidation,
14
+ type EvaluateRevalidationOptions,
15
+ } from "../revalidation.js";
13
16
  import {
14
17
  getParallelEntries,
15
18
  getParallelSlotEntries,
@@ -38,8 +41,8 @@ import { entryLoadingMasksLoaders } from "./loader-mask.js";
38
41
  import {
39
42
  defersLoader,
40
43
  deferUnit,
44
+ fillSends,
41
45
  getPrefetchDeferral,
42
- isFillRequest,
43
46
  registerDeferredLoader,
44
47
  } from "./prefetch-deferral.js";
45
48
  import {
@@ -110,6 +113,28 @@ function emitRevalidationDecision(
110
113
  });
111
114
  }
112
115
 
116
+ /**
117
+ * Whether a segment the client holds (`held`, the set resolution works on)
118
+ * renders again. A fill keeps it (prefetch-deferral.ts fillSends): no
119
+ * revalidate() predicate runs and nothing is traced or observed. Any other
120
+ * request asks the segment's revalidate() chain, and `onEvaluated` sees that
121
+ * answer.
122
+ */
123
+ async function revalidateHeld<TEnv>({
124
+ held,
125
+ onEvaluated,
126
+ ...options
127
+ }: EvaluateRevalidationOptions<TEnv> & {
128
+ held: ReadonlySet<string>;
129
+ onEvaluated?: (shouldRevalidate: boolean) => void;
130
+ }): Promise<boolean> {
131
+ const fill = fillSends(options.context, options.segment.id, held);
132
+ if (fill !== undefined) return fill;
133
+ const shouldRevalidate = await evaluateRevalidation(options);
134
+ onEvaluated?.(shouldRevalidate);
135
+ return shouldRevalidate;
136
+ }
137
+
113
138
  // ---------------------------------------------------------------------------
114
139
  // prefetch: false (prefetch-deferral.ts)
115
140
  // ---------------------------------------------------------------------------
@@ -194,7 +219,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
194
219
  }));
195
220
 
196
221
  const matchedIds = loaderMeta.map((m) => m.segmentId);
197
- const fill = isFillRequest(ctx);
198
222
 
199
223
  const revalidationChecks = await Promise.all(
200
224
  loaderMeta.map(
@@ -221,8 +245,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
221
245
  }
222
246
  return true;
223
247
  }
224
- // Fill: a held segment is skipped outright, no predicate runs.
225
- if (fill) return false;
226
248
 
227
249
  const dummySegment: ResolvedSegment = {
228
250
  id: segmentId,
@@ -235,7 +257,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
235
257
  belongsToRoute,
236
258
  };
237
259
 
238
- return await evaluateRevalidation({
260
+ return await revalidateHeld({
239
261
  segment: dummySegment,
240
262
  prevParams,
241
263
  getPrevSegment: null,
@@ -251,6 +273,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
251
273
  actionContext,
252
274
  stale,
253
275
  traceSource: "loader",
276
+ held: clientSegmentIds,
254
277
  });
255
278
  },
256
279
  async () => true,
@@ -704,10 +727,12 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
704
727
  // runs: a prefetch sends what the slot declares instead of running its
705
728
  // handler.
706
729
  let deferred = deferUnit(context, parallelId, "below");
730
+ // A fill sends exactly the slots the client lacks, ahead of every rule
731
+ // below (fillSends).
732
+ const fill = fillSends(context, parallelId, clientSegmentIds);
707
733
  let shouldResolve: boolean;
708
- if (isFillRequest(context)) {
709
- // Fill: only what the client does not hold renders, no predicate runs.
710
- shouldResolve = !clientSegmentIds.has(parallelId);
734
+ if (fill !== undefined) {
735
+ shouldResolve = fill;
711
736
  } else if (deferred) {
712
737
  shouldResolve = true;
713
738
  } else if (isFullRefetch) {
@@ -880,8 +905,6 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
880
905
  }
881
906
  return true;
882
907
  }
883
- // Fill: a held segment is skipped outright, no predicate runs.
884
- if (isFillRequest(context)) return false;
885
908
 
886
909
  const dummySegment: ResolvedSegment = {
887
910
  id: entry.shortCode,
@@ -900,7 +923,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
900
923
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
901
924
  };
902
925
 
903
- const shouldRevalidate = await evaluateRevalidation({
926
+ return revalidateHeld({
904
927
  segment: dummySegment,
905
928
  prevParams,
906
929
  getPrevSegment: null,
@@ -917,18 +940,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
917
940
  stale,
918
941
  traceSource:
919
942
  entry.type === "route" ? "route-handler" : "layout-handler",
943
+ held: clientSegmentIds,
944
+ onEvaluated: (shouldRevalidate) => {
945
+ emitRevalidationDecision(
946
+ entry.shortCode,
947
+ context.pathname,
948
+ routeKey,
949
+ shouldRevalidate,
950
+ );
951
+ debugLog("segment.revalidate", "entry revalidation decision", {
952
+ segmentId: entry.shortCode,
953
+ shouldRevalidate,
954
+ });
955
+ },
920
956
  });
921
- emitRevalidationDecision(
922
- entry.shortCode,
923
- context.pathname,
924
- routeKey,
925
- shouldRevalidate,
926
- );
927
- debugLog("segment.revalidate", "entry revalidation decision", {
928
- segmentId: entry.shortCode,
929
- shouldRevalidate,
930
- });
931
- return shouldRevalidate;
932
957
  },
933
958
  async () => {
934
959
  // Only an entry the client lacks (defersUnit). Its handler would
@@ -1308,8 +1333,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1308
1333
  }
1309
1334
  return true;
1310
1335
  }
1311
- // Fill: a held segment is skipped outright, no predicate runs.
1312
- if (isFillRequest(context)) return false;
1313
1336
 
1314
1337
  const dummySegment: ResolvedSegment = {
1315
1338
  id: orphan.shortCode,
@@ -1323,7 +1346,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1323
1346
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1324
1347
  };
1325
1348
 
1326
- const shouldRevalidate = await evaluateRevalidation({
1349
+ return revalidateHeld({
1327
1350
  segment: dummySegment,
1328
1351
  prevParams,
1329
1352
  getPrevSegment: null,
@@ -1339,14 +1362,15 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1339
1362
  actionContext,
1340
1363
  stale,
1341
1364
  traceSource: "orphan-layout",
1365
+ held: clientSegmentIds,
1366
+ onEvaluated: (shouldRevalidate) =>
1367
+ emitRevalidationDecision(
1368
+ orphan.shortCode,
1369
+ context.pathname,
1370
+ routeKey,
1371
+ shouldRevalidate,
1372
+ ),
1342
1373
  });
1343
- emitRevalidationDecision(
1344
- orphan.shortCode,
1345
- context.pathname,
1346
- routeKey,
1347
- shouldRevalidate,
1348
- );
1349
- return shouldRevalidate;
1350
1374
  },
1351
1375
  async () => {
1352
1376
  // Below a unit a prefetch skipped: the orphan goes with it.
@@ -14,7 +14,11 @@ import type { HandlerContext } from "./handler-context.js";
14
14
  import type { RequestContext } from "../server/request-context.js";
15
15
  import type { HandleStore } from "../server/handle-store.js";
16
16
  import type { Theme } from "../theme/types.js";
17
- import { documentCacheStoresRender } from "../cache/document-cache.js";
17
+ import {
18
+ rendersForSharedAudience,
19
+ rewriteTrail,
20
+ visitorUrl,
21
+ } from "../server/rewrite-trail.js";
18
22
  import { attachTransitionWhen } from "./attach-transition-when.js";
19
23
  import { publicRouteName } from "../route-name.js";
20
24
  import { resolvedHandleStream } from "../handles/deferred-resolution.js";
@@ -38,7 +42,7 @@ export function buildFullPayload(
38
42
  ): RscPayload {
39
43
  return {
40
44
  metadata: {
41
- pathname: url.pathname,
45
+ pathname: renderedLocation(reqCtx, url).pathname,
42
46
  routerId: ctx.router.id,
43
47
  basename: ctx.router.basename,
44
48
  segments: attachTransitionWhen(m.segments, reqCtx),
@@ -84,6 +88,36 @@ export function buildFullPayload(
84
88
  };
85
89
  }
86
90
 
91
+ /**
92
+ * The URL a render shows as the page's location (payload `pathname`, the SSR
93
+ * search and origin seed). For a request middleware rewrote, the visitor's
94
+ * URL, so the server markup and the hydrating client (which reads the address
95
+ * bar) agree on `usePathname()` and `useSearchParams()`. A render other
96
+ * visitors may be served shows `url`, the target, as a direct visit does: a
97
+ * shell capture, and a render the document cache stores (its key is the
98
+ * target). A render that shows the visitor's URL marks
99
+ * `_payloadVisitorLocation`, and the document cache does not store it.
100
+ */
101
+ export function renderedLocation(
102
+ reqCtx:
103
+ | Pick<
104
+ RequestContext<any>,
105
+ | "_variables"
106
+ | "_shellCaptureRun"
107
+ | "_documentCacheRender"
108
+ | "_payloadVisitorLocation"
109
+ | "res"
110
+ >
111
+ | undefined,
112
+ url: URL,
113
+ ): URL {
114
+ if (!reqCtx || !rewriteTrail(reqCtx) || rendersForSharedAudience(reqCtx)) {
115
+ return url;
116
+ }
117
+ reqCtx._payloadVisitorLocation = true;
118
+ return visitorUrl(reqCtx, url);
119
+ }
120
+
87
121
  /**
88
122
  * The theme a document payload carries as `metadata.initialTheme` (the
89
123
  * ThemeProvider's first state), read with no identity guard: it is the
@@ -116,9 +150,7 @@ export function payloadInitialTheme(
116
150
  ): Theme | undefined {
117
151
  const config = reqCtx._themeConfig;
118
152
  if (!config) return undefined;
119
- if (reqCtx._shellCaptureRun || documentCacheStoresRender(reqCtx)) {
120
- return config.defaultTheme;
121
- }
153
+ if (rendersForSharedAudience(reqCtx)) return config.defaultTheme;
122
154
  const theme = reqCtx._readTheme();
123
155
  if (theme !== config.defaultTheme) reqCtx._payloadVisitorTheme = true;
124
156
  return theme;