@rangojs/router 0.24.4 → 0.26.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 (301) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/rango.js +363 -39
  3. package/dist/testing/vitest.js +37 -4
  4. package/dist/types/browser/event-controller.d.ts +24 -5
  5. package/dist/types/browser/intercept-utils.d.ts +5 -2
  6. package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
  7. package/dist/types/browser/navigation-transaction.d.ts +5 -5
  8. package/dist/types/browser/partial-update.d.ts +21 -13
  9. package/dist/types/browser/prefetch/cache.d.ts +8 -0
  10. package/dist/types/browser/rango-marker.d.ts +7 -0
  11. package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
  12. package/dist/types/browser/react/use-handle.d.ts +20 -0
  13. package/dist/types/browser/react/use-href.d.ts +6 -0
  14. package/dist/types/browser/react/use-router.d.ts +6 -1
  15. package/dist/types/browser/response-adapter.d.ts +5 -3
  16. package/dist/types/browser/scroll-restoration.d.ts +9 -3
  17. package/dist/types/browser/segment-reconciler.d.ts +10 -13
  18. package/dist/types/browser/types.d.ts +38 -8
  19. package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  22. package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
  23. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  24. package/dist/types/cache/types.d.ts +16 -6
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  26. package/dist/types/client-urls/client-urls.d.ts +6 -2
  27. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  28. package/dist/types/client-urls/server-projection.d.ts +9 -5
  29. package/dist/types/client-urls/types.d.ts +29 -16
  30. package/dist/types/client.d.ts +2 -2
  31. package/dist/types/client.rsc.d.ts +2 -2
  32. package/dist/types/decode-loader-results.d.ts +28 -7
  33. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  34. package/dist/types/handles/meta.d.ts +5 -0
  35. package/dist/types/href-client.d.ts +3 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/loader-redirect.d.ts +2 -3
  38. package/dist/types/outlet-context.d.ts +7 -6
  39. package/dist/types/prefetch-markers.d.ts +14 -5
  40. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  41. package/dist/types/prerender/writable-store.d.ts +2 -3
  42. package/dist/types/route-content-wrapper.d.ts +17 -14
  43. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  44. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  45. package/dist/types/route-definition/rewrite.d.ts +48 -5
  46. package/dist/types/route-types.d.ts +2 -2
  47. package/dist/types/router/handler-context.d.ts +14 -2
  48. package/dist/types/router/intercept-resolution.d.ts +19 -46
  49. package/dist/types/router/lazy-includes.d.ts +7 -0
  50. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  51. package/dist/types/router/middleware.d.ts +0 -16
  52. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  53. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  54. package/dist/types/router/revalidation.d.ts +8 -1
  55. package/dist/types/router/router-context.d.ts +4 -16
  56. package/dist/types/router/router-interfaces.d.ts +0 -11
  57. package/dist/types/router/router-options.d.ts +4 -27
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  59. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  60. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  61. package/dist/types/router/segment-resolution.d.ts +1 -1
  62. package/dist/types/router/segment-wrappers.d.ts +3 -5
  63. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  64. package/dist/types/router/tracing.d.ts +2 -2
  65. package/dist/types/router-versions.d.ts +8 -6
  66. package/dist/types/rsc/full-payload.d.ts +14 -2
  67. package/dist/types/rsc/helpers.d.ts +8 -0
  68. package/dist/types/rsc/manifest-init.d.ts +20 -0
  69. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  70. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  71. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  72. package/dist/types/rsc/server-action.d.ts +6 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +14 -4
  74. package/dist/types/rsc/shell-capture.d.ts +23 -2
  75. package/dist/types/rsc/types.d.ts +5 -8
  76. package/dist/types/segment-system.d.ts +15 -0
  77. package/dist/types/server/build-version-table.d.ts +10 -13
  78. package/dist/types/server/context.d.ts +90 -6
  79. package/dist/types/server/handle-store.d.ts +17 -6
  80. package/dist/types/server/request-context.d.ts +45 -7
  81. package/dist/types/server/request-headers.d.ts +12 -0
  82. package/dist/types/server/rewrite-trail.d.ts +8 -1
  83. package/dist/types/testing/build-versions.d.ts +0 -3
  84. package/dist/types/testing/e2e/index.d.ts +1 -1
  85. package/dist/types/testing/flight.entry.d.ts +1 -1
  86. package/dist/types/testing/index.d.ts +1 -1
  87. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  88. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  89. package/dist/types/testing/render-route.d.ts +21 -11
  90. package/dist/types/testing/run-loader.d.ts +7 -0
  91. package/dist/types/testing/run-middleware.d.ts +7 -0
  92. package/dist/types/testing/run-revalidate.d.ts +5 -4
  93. package/dist/types/testing/serve-shell-request.d.ts +42 -5
  94. package/dist/types/testing/shell-status.d.ts +17 -0
  95. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  96. package/dist/types/testing/vitest.d.ts +10 -1
  97. package/dist/types/thenable-status.d.ts +0 -6
  98. package/dist/types/types/handler-context.d.ts +12 -1
  99. package/dist/types/types/loader-types.d.ts +10 -4
  100. package/dist/types/types/route-entry.d.ts +2 -1
  101. package/dist/types/urls/path-helper-types.d.ts +4 -2
  102. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  103. package/dist/types/vite/discovery/state.d.ts +20 -0
  104. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  105. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  106. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  107. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  108. package/dist/vite/index.js +6819 -6555
  109. package/package.json +1 -1
  110. package/skills/app-structure/SKILL.md +255 -0
  111. package/skills/app-structure/content-site.md +333 -0
  112. package/skills/app-structure/e-commerce.md +435 -0
  113. package/skills/app-structure/large-app.md +885 -0
  114. package/skills/app-structure/multi-app.md +280 -0
  115. package/skills/breadcrumbs/SKILL.md +6 -1
  116. package/skills/cache-guide/SKILL.md +4 -8
  117. package/skills/caching/SKILL.md +6 -6
  118. package/skills/catalog.json +12 -0
  119. package/skills/client-urls/SKILL.md +185 -32
  120. package/skills/comparison/references/framework-comparison.md +1 -1
  121. package/skills/composability/SKILL.md +10 -8
  122. package/skills/handler-use/SKILL.md +48 -9
  123. package/skills/hooks/handle-and-actions.md +104 -0
  124. package/skills/hooks/navigation.md +24 -10
  125. package/skills/hooks/urls.md +1 -1
  126. package/skills/host-router/SKILL.md +1 -1
  127. package/skills/i18n/SKILL.md +2 -1
  128. package/skills/intercept/SKILL.md +60 -29
  129. package/skills/layout/SKILL.md +6 -3
  130. package/skills/links/SKILL.md +151 -17
  131. package/skills/loader/SKILL.md +42 -19
  132. package/skills/middleware/SKILL.md +111 -15
  133. package/skills/observability/SKILL.md +11 -0
  134. package/skills/parallel/SKILL.md +5 -5
  135. package/skills/ppr/SKILL.md +103 -52
  136. package/skills/prerender/SKILL.md +4 -5
  137. package/skills/rango/SKILL.md +3 -0
  138. package/skills/response-routes/SKILL.md +4 -1
  139. package/skills/router-setup/SKILL.md +0 -2
  140. package/skills/scripts/SKILL.md +12 -1
  141. package/skills/server-actions/SKILL.md +60 -20
  142. package/skills/testing/SKILL.md +1 -0
  143. package/skills/testing/client-components.md +13 -10
  144. package/skills/testing/loader.md +25 -24
  145. package/skills/testing/middleware.md +11 -11
  146. package/skills/testing/render-handler.md +17 -17
  147. package/skills/testing/response-routes.md +4 -1
  148. package/skills/testing/serve-shell-request.md +86 -22
  149. package/skills/testing/server-actions.md +16 -16
  150. package/skills/ux-architecture/SKILL.md +980 -0
  151. package/skills/vercel/SKILL.md +1 -1
  152. package/skills/view-transitions/SKILL.md +43 -10
  153. package/src/__augment-tests__/augmented.check.ts +28 -0
  154. package/src/browser/event-controller.ts +44 -18
  155. package/src/browser/intercept-utils.ts +3 -2
  156. package/src/browser/merge-segment-loaders.ts +0 -85
  157. package/src/browser/navigation-bridge.ts +112 -84
  158. package/src/browser/navigation-client.ts +20 -15
  159. package/src/browser/navigation-store.ts +4 -2
  160. package/src/browser/navigation-transaction.ts +9 -8
  161. package/src/browser/partial-update.ts +90 -32
  162. package/src/browser/prefetch/cache.ts +8 -0
  163. package/src/browser/prefetch/fetch.ts +13 -3
  164. package/src/browser/rango-marker.ts +13 -0
  165. package/src/browser/react/NavigationProvider.tsx +25 -7
  166. package/src/browser/react/filter-segment-order.ts +13 -0
  167. package/src/browser/react/use-action.ts +9 -15
  168. package/src/browser/react/use-handle.ts +63 -71
  169. package/src/browser/react/use-href.tsx +24 -5
  170. package/src/browser/react/use-link-status.ts +1 -1
  171. package/src/browser/react/use-navigation.ts +30 -40
  172. package/src/browser/react/use-router.ts +37 -6
  173. package/src/browser/react/use-segments.ts +8 -12
  174. package/src/browser/response-adapter.ts +11 -5
  175. package/src/browser/rsc-router.tsx +3 -1
  176. package/src/browser/scroll-restoration.ts +34 -20
  177. package/src/browser/segment-reconciler.ts +11 -29
  178. package/src/browser/server-action-bridge.ts +52 -16
  179. package/src/browser/types.ts +40 -8
  180. package/src/build/generate-manifest.ts +12 -1
  181. package/src/build/route-types/codegen.ts +6 -2
  182. package/src/build/route-types/per-module-writer.ts +94 -61
  183. package/src/cache/cache-runtime.ts +9 -0
  184. package/src/cache/cache-scope.ts +2 -1
  185. package/src/cache/cf/cf-cache-store.ts +6 -7
  186. package/src/cache/cf/cf-cache-types.ts +0 -17
  187. package/src/cache/cf/cf-shell-frame.ts +3 -0
  188. package/src/cache/handle-snapshot.ts +2 -2
  189. package/src/cache/types.ts +16 -6
  190. package/src/cache/vercel/vercel-cache-store.ts +8 -19
  191. package/src/client-urls/client-urls.ts +202 -153
  192. package/src/client-urls/revalidate-registry.ts +12 -3
  193. package/src/client-urls/server-projection.ts +137 -66
  194. package/src/client-urls/types.ts +30 -15
  195. package/src/client.rsc.tsx +11 -1
  196. package/src/client.tsx +9 -1
  197. package/src/decode-loader-results.ts +64 -31
  198. package/src/handles/deferred-resolution.ts +3 -2
  199. package/src/handles/meta.ts +5 -0
  200. package/src/href-client.ts +3 -1
  201. package/src/index.rsc.ts +1 -0
  202. package/src/index.ts +12 -4
  203. package/src/loader-redirect.tsx +2 -3
  204. package/src/outlet-context.ts +7 -6
  205. package/src/prefetch-markers.ts +15 -5
  206. package/src/prerender/build-shell-capture.ts +1 -2
  207. package/src/prerender/writable-store.ts +2 -3
  208. package/src/reverse.ts +3 -3
  209. package/src/route-content-wrapper.tsx +19 -17
  210. package/src/route-definition/dsl-helpers.ts +41 -21
  211. package/src/route-definition/helpers-types.ts +7 -2
  212. package/src/route-definition/resolve-handler-use.ts +23 -10
  213. package/src/route-definition/rewrite.ts +86 -19
  214. package/src/route-types.ts +1 -2
  215. package/src/router/find-match.ts +37 -1
  216. package/src/router/handler-context.ts +85 -40
  217. package/src/router/intercept-resolution.ts +161 -307
  218. package/src/router/lazy-includes.ts +43 -27
  219. package/src/router/loader-resolution.ts +21 -1
  220. package/src/router/manifest.ts +15 -10
  221. package/src/router/match-api.ts +11 -1
  222. package/src/router/match-middleware/background-revalidation.ts +2 -4
  223. package/src/router/match-middleware/cache-lookup.ts +16 -20
  224. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  225. package/src/router/match-result.ts +31 -2
  226. package/src/router/middleware.ts +6 -147
  227. package/src/router/navigation-snapshot.ts +38 -2
  228. package/src/router/prefetch-cache-ttl.ts +10 -18
  229. package/src/router/prerender-match.ts +2 -1
  230. package/src/router/revalidation.ts +24 -3
  231. package/src/router/router-context.ts +5 -18
  232. package/src/router/router-interfaces.ts +0 -13
  233. package/src/router/router-options.ts +4 -28
  234. package/src/router/segment-resolution/fresh.ts +6 -2
  235. package/src/router/segment-resolution/helpers.ts +7 -1
  236. package/src/router/segment-resolution/loader-cache.ts +4 -3
  237. package/src/router/segment-resolution/mask-nested.ts +5 -3
  238. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  239. package/src/router/segment-resolution/revalidation.ts +75 -35
  240. package/src/router/segment-resolution.ts +0 -1
  241. package/src/router/segment-wrappers.ts +8 -13
  242. package/src/router/substitute-pattern-params.ts +3 -2
  243. package/src/router/tracing.ts +2 -2
  244. package/src/router-versions.ts +7 -7
  245. package/src/router.ts +5 -17
  246. package/src/rsc/full-payload.ts +26 -4
  247. package/src/rsc/handler.ts +86 -48
  248. package/src/rsc/helpers.ts +13 -0
  249. package/src/rsc/manifest-init.ts +56 -3
  250. package/src/rsc/prefetch-render-error.ts +38 -0
  251. package/src/rsc/progressive-enhancement.ts +161 -211
  252. package/src/rsc/rewrite-passes.ts +56 -9
  253. package/src/rsc/rsc-rendering.ts +79 -23
  254. package/src/rsc/server-action.ts +25 -2
  255. package/src/rsc/shell-capture-constants.ts +26 -3
  256. package/src/rsc/shell-capture.ts +137 -51
  257. package/src/rsc/types.ts +5 -9
  258. package/src/segment-system.tsx +125 -119
  259. package/src/server/build-version-table.ts +12 -24
  260. package/src/server/context.ts +160 -7
  261. package/src/server/handle-store.ts +80 -12
  262. package/src/server/request-context.ts +46 -6
  263. package/src/server/request-headers.ts +32 -0
  264. package/src/server/rewrite-trail.ts +55 -9
  265. package/src/ssr/ssr-root.tsx +6 -10
  266. package/src/testing/build-versions.ts +0 -3
  267. package/src/testing/dispatch.ts +5 -8
  268. package/src/testing/e2e/index.ts +1 -0
  269. package/src/testing/flight.entry.ts +1 -0
  270. package/src/testing/index.ts +1 -0
  271. package/src/testing/internal/client-urls-projection.ts +31 -11
  272. package/src/testing/internal/seed-vars.ts +4 -4
  273. package/src/testing/render-route.tsx +83 -40
  274. package/src/testing/run-loader.ts +26 -2
  275. package/src/testing/run-middleware.ts +10 -0
  276. package/src/testing/run-revalidate.ts +13 -6
  277. package/src/testing/serve-shell-request.ts +79 -9
  278. package/src/testing/shell-status.ts +40 -0
  279. package/src/testing/vitest-stubs/version.ts +1 -0
  280. package/src/testing/vitest.ts +74 -7
  281. package/src/thenable-status.ts +0 -11
  282. package/src/types/handler-context.ts +12 -1
  283. package/src/types/loader-types.ts +10 -4
  284. package/src/types/route-entry.ts +2 -1
  285. package/src/urls/include-helper.ts +38 -11
  286. package/src/urls/path-helper-types.ts +4 -2
  287. package/src/vite/discovery/client-urls-projection.ts +205 -16
  288. package/src/vite/discovery/discover-routers.ts +2 -2
  289. package/src/vite/discovery/prerender-collection.ts +2 -6
  290. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
  291. package/src/vite/discovery/state.ts +26 -0
  292. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  293. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  294. package/src/vite/plugins/hoist/source.ts +8 -7
  295. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  296. package/src/vite/plugins/version-plugin.ts +7 -0
  297. package/src/vite/plugins/version.d.ts +7 -0
  298. package/src/vite/plugins/virtual-entries.ts +20 -0
  299. package/src/vite/rango.ts +4 -1
  300. package/src/vite/router-discovery.ts +39 -7
  301. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -40,6 +40,7 @@ import {
40
40
  renderRscFlightStage,
41
41
  renderRscResponse,
42
42
  } from "./render-pipeline.js";
43
+ import { markPrefetchOnRenderError } from "./prefetch-render-error.js";
43
44
  import {
44
45
  createRoutineTrace,
45
46
  handoff,
@@ -51,7 +52,7 @@ import {
51
52
  import type { HandlerContext } from "./handler-context.js";
52
53
  import { attachTransitionWhen } from "./attach-transition-when.js";
53
54
  import { publicRouteName } from "../route-name.js";
54
- import { buildFullPayload, renderedLocation } from "./full-payload.js";
55
+ import { buildFullPayload, payloadLocation } from "./full-payload.js";
55
56
  import {
56
57
  capShellWindow,
57
58
  lastStoredCaptureSeq,
@@ -94,6 +95,8 @@ import {
94
95
  type DevShellLookup,
95
96
  } from "./shell-build-manifest.js";
96
97
  import { contextGet } from "../context-var.js";
98
+ import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
99
+ import { visitorShellKey } from "./shell-capture-constants.js";
97
100
  import { PREFETCH_HEADER } from "../prefetch-markers.js";
98
101
  import {
99
102
  isWarmReplace,
@@ -155,10 +158,52 @@ function describePprReplayStatus(status: PprReplayStatus): string {
155
158
  : `bypass:${status.reason}`;
156
159
  }
157
160
 
161
+ /**
162
+ * The shell key before the route's own partition, or undefined: a request
163
+ * middleware rewrote reads and captures no shell unless its rewrite chose
164
+ * `.shell()`, so a URL's shared shell is read and written only by requests
165
+ * on that URL (#1146).
166
+ */
167
+ interface RequestShellBase {
168
+ baseKey: string;
169
+ /** shellSearchSeed of the URL the key names. */
170
+ searchSeed: string;
171
+ /** A visitor-keyed shell's location (ShellCacheEntry.pathname). */
172
+ pathname?: string;
173
+ }
174
+
175
+ function requestShellBase(
176
+ ctx: HandlerContext<any>,
177
+ reqCtx: RequestContext<any>,
178
+ url: URL,
179
+ ): RequestShellBase | undefined {
180
+ const filter = reqCtx._searchParamsFilter;
181
+ const trail = rewriteTrail(reqCtx);
182
+ const shell = rewriteShellAudience(reqCtx);
183
+ if (trail && shell === "visitor") {
184
+ const visitor = new URL(trail[0]!);
185
+ return {
186
+ baseKey: visitorShellKey(
187
+ ctx.router.id,
188
+ visitor,
189
+ new URL(trail[trail.length - 1]!),
190
+ filter,
191
+ ),
192
+ searchSeed: shellSearchSeed(visitor, filter),
193
+ pathname: visitor.pathname,
194
+ };
195
+ }
196
+ if (trail && shell !== "target") return undefined;
197
+ return {
198
+ baseKey: buildShellKey(ctx.router.id, url, filter),
199
+ searchSeed: shellSearchSeed(url, filter),
200
+ };
201
+ }
202
+
158
203
  function createShellCaptureDescriptor(
159
204
  ctx: HandlerContext<any>,
160
205
  key: string,
161
- searchSeed: string,
206
+ base: RequestShellBase,
162
207
  originSeed: string,
163
208
  pprConfig: ResolvedPprConfig,
164
209
  store: SegmentCacheStore<any>,
@@ -166,7 +211,8 @@ function createShellCaptureDescriptor(
166
211
  ): ShellCaptureDescriptor {
167
212
  return {
168
213
  key,
169
- searchSeed,
214
+ searchSeed: base.searchSeed,
215
+ pathname: base.pathname,
170
216
  // Derived from the same request URL as the key (the callers compute
171
217
  // both next to each other, so they cannot drift): seeding the origin
172
218
  // keeps origin-dependent static markup (Link's data-external)
@@ -499,13 +545,15 @@ function* shellServePlan<TEnv>(
499
545
  // no shell read, no capture, so it can never degrade again. The handler
500
546
  // stripped the marker from the request and left this flag.
501
547
  if (reqCtx._shellForcedMiss) return { kind: "pass" };
548
+ const base = requestShellBase(ctx, reqCtx, url);
549
+ if (!base) return { kind: "pass" };
502
550
 
503
551
  // A per-request CSP nonce pins the route to axis 1: a shared shell would
504
552
  // freeze the capture request's nonce and CSP would reject it for every
505
553
  // other visitor. Every step below, a HIT included, therefore runs without
506
554
  // one.
507
555
  const store = reqCtx._cacheStore;
508
- const baseKey = buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter);
556
+ const { baseKey } = base;
509
557
  if (activeRequestNonce(nonce, reqCtx) !== undefined) {
510
558
  // Declared intent that cannot be honored deserves a diagnostic (unlike an
511
559
  // undeclared route, which is silent): a ppr route gated off by an active
@@ -596,7 +644,7 @@ function* shellServePlan<TEnv>(
596
644
  const descriptor = createShellCaptureDescriptor(
597
645
  ctx,
598
646
  key,
599
- shellSearchSeed(url, reqCtx._searchParamsFilter),
647
+ base,
600
648
  url.origin,
601
649
  pprConfig,
602
650
  store,
@@ -626,10 +674,13 @@ function* shellServePlan<TEnv>(
626
674
  // A HIT replays the handler layer from the entry's doc record (`docKey`),
627
675
  // or, for a Prerender route, from the prerender store. An entry that has
628
676
  // neither is a MISS: serving it would run handlers after the commit.
677
+ // The entry must have rendered this request's location: the browser
678
+ // hydrates against the address bar.
629
679
  if (
630
680
  cached &&
631
681
  !cached.entry.navigationOnly &&
632
682
  (cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
683
+ cached.entry.pathname === base.pathname &&
633
684
  isValidShellHit(cached.entry, ctx.version)
634
685
  ) {
635
686
  const document = openShellDocumentMetered(reqCtx, cached);
@@ -713,7 +764,9 @@ function* shellServePlan<TEnv>(
713
764
  // compile-time constant).
714
765
  const devShellLookup = resolveDevShellLookup(reqCtx, pprConfig);
715
766
  let buildHit: Awaited<ReturnType<typeof lookupBuildShell>> = null;
716
- if (key !== baseKey) {
767
+ if (base.pathname !== undefined) {
768
+ // A build shell is the target's: never served under a visitor key.
769
+ } else if (key !== baseKey) {
717
770
  // Probed once per path without a build shell, and not again once the
718
771
  // route warned. A search-bearing URL never reads one: nothing to probe.
719
772
  const routeName = reqCtx._classifiedRoute?.routeKey;
@@ -845,7 +898,7 @@ function* preparePayloadPlan<TEnv>(
845
898
  kind: "payload",
846
899
  payload: {
847
900
  metadata: {
848
- pathname: renderedLocation(reqCtx, url).pathname,
901
+ ...payloadLocation(reqCtx, url),
849
902
  // routerId is serialized on every payload (including within-session
850
903
  // ones) so the frontend can read the current app/router identity. It
851
904
  // always equals the current app's id: a cross-app navigation is
@@ -1209,16 +1262,6 @@ function renderPreparedRscResponse<TEnv>(
1209
1262
  if (sourceScoped) {
1210
1263
  rscHeaders["x-rsc-prefetch-scope"] = "source";
1211
1264
  }
1212
- // Enable browser HTTP caching for prefetch responses only.
1213
- // Requires a prefetch request (X-Rango-Prefetch, sent by Link prefetch
1214
- // fetch), a source-agnostic response (see sourceScoped above), and a
1215
- // configured cache-control value (false disables caching).
1216
- if (reqCtx._requestKind === "prefetch" && isPartial && !sourceScoped) {
1217
- const cc = ctx.router.prefetchCacheControl;
1218
- if (cc) {
1219
- rscHeaders["cache-control"] = cc;
1220
- }
1221
- }
1222
1265
  // prefetch: false (docs/design/prefetch-false.md, R7). A response that
1223
1266
  // carries deferred units answers one page's prefetch, and a fill is the
1224
1267
  // second half of one adoption: neither is reused by a shared cache or the
@@ -1248,7 +1291,7 @@ function renderPreparedRscResponse<TEnv>(
1248
1291
  reqCtx._activeRoutine &&
1249
1292
  createRenderStageTraceBridge(reqCtx._activeRoutine),
1250
1293
  };
1251
- return renderRscResponse(
1294
+ const rendered = renderRscResponse(
1252
1295
  {
1253
1296
  ctx,
1254
1297
  request,
@@ -1272,6 +1315,11 @@ function renderPreparedRscResponse<TEnv>(
1272
1315
  }),
1273
1316
  },
1274
1317
  );
1318
+ // A prefetch whose render reported an error is not kept (#1170).
1319
+ if (reqCtx._requestKind !== "prefetch" || !isPartial) return rendered;
1320
+ return rendered.then((response) =>
1321
+ markPrefetchOnRenderError(response, reqCtx._renderErrors ?? []),
1322
+ );
1275
1323
  }
1276
1324
 
1277
1325
  /**
@@ -1286,6 +1334,8 @@ function scheduleNavigationShellCapture<TEnv>(
1286
1334
  const { ctx, request, env, url, reqCtx } = input;
1287
1335
  const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
1288
1336
  const store = reqCtx._cacheStore!;
1337
+ const base = requestShellBase(ctx, reqCtx, url);
1338
+ if (!base) return;
1289
1339
  scheduleShellCapture(
1290
1340
  ctx,
1291
1341
  request,
@@ -1305,7 +1355,7 @@ function scheduleNavigationShellCapture<TEnv>(
1305
1355
  createShellCaptureDescriptor(
1306
1356
  ctx,
1307
1357
  captureKey,
1308
- shellSearchSeed(url, reqCtx._searchParamsFilter),
1358
+ base,
1309
1359
  url.origin,
1310
1360
  pprConfig,
1311
1361
  store,
@@ -1464,10 +1514,13 @@ async function matchPartialWithPprReplay<TEnv>(
1464
1514
  return runMatch({ outcome: "BYPASS", reason: "cache-disabled" });
1465
1515
  }
1466
1516
 
1517
+ const base = requestShellBase(ctx, reqCtx, url);
1518
+ if (!base) return runMatch({ outcome: "BYPASS", reason: "rewritten" });
1519
+
1467
1520
  // The visitor's own partition (resolveShellPartition): replay never
1468
1521
  // crosses into another partition's shell.
1469
1522
  const key = await resolveRequestShellKey(
1470
- buildShellKey(ctx.router.id, url, reqCtx._searchParamsFilter),
1523
+ base.baseKey,
1471
1524
  routeSnapshot,
1472
1525
  store,
1473
1526
  reqCtx,
@@ -1806,6 +1859,9 @@ function serveShellHit(
1806
1859
  descriptor: ShellCaptureDescriptor,
1807
1860
  ): Response {
1808
1861
  const { entry, prelude: preludeBytes } = document;
1862
+ // A visitor-keyed shell shows the visitor's URL: the document cache keys
1863
+ // this response by the target.
1864
+ if (entry.pathname !== undefined) reqCtx._payloadVisitorLocation = true;
1809
1865
  const metricsStore = reqCtx._metricsStore;
1810
1866
  // Per-stage tail timing for the `ppr:tail` Server-Timing mirror and the
1811
1867
  // perf console line: always in dev, in production only when this request
@@ -1867,8 +1923,8 @@ function serveShellHit(
1867
1923
  payload.metadata.initialTheme = entry.initialTheme as
1868
1924
  | import("../theme/types.js").Theme
1869
1925
  | undefined;
1870
- // Same for the location: the capture rendered the target's.
1871
- payload.metadata.pathname = url.pathname;
1926
+ // Same for the location: the key's URL, or a visitor-keyed shell's.
1927
+ payload.metadata.pathname = entry.pathname ?? url.pathname;
1872
1928
  }
1873
1929
  // Full Flight render per request: hydration needs the whole payload (there
1874
1930
  // is no Flight-side resume — a React limitation, not ours).
@@ -1911,7 +1967,7 @@ function serveShellHit(
1911
1967
  nonce: undefined,
1912
1968
  // The shell key's own search — identical to the capture seed for
1913
1969
  // this key, so the resume tree matches the captured tree.
1914
- search: shellSearchSeed(url, reqCtx._searchParamsFilter),
1970
+ search: descriptor.searchSeed,
1915
1971
  // The HIT request's origin — same host as the capture's (key-scoped).
1916
1972
  origin: url.origin,
1917
1973
  // The document cache reads the list before storing this composite.
@@ -24,6 +24,7 @@ import { observePhase, PHASES } from "../router/instrument.js";
24
24
  import { resolveActionRefId } from "../router/is-action.js";
25
25
  import type { TraceSpan } from "../router/tracing.js";
26
26
  import { attachTransitionWhen } from "./attach-transition-when.js";
27
+ import { payloadLocation } from "./full-payload.js";
27
28
  import { publicRouteName } from "../route-name.js";
28
29
  import type { RscPayload } from "./types.js";
29
30
  import {
@@ -323,6 +324,28 @@ export async function executeServerAction<TEnv>(
323
324
  };
324
325
  }
325
326
 
327
+ /**
328
+ * The continuation for a rewrite pass after the action ran (#1145): a failed
329
+ * action's error boundary is re-matched on the target, since the one pass 1
330
+ * matched (or missed) is the rewriting route's.
331
+ */
332
+ export async function continuationForRewritePass<TEnv>(
333
+ ctx: HandlerContext<TEnv>,
334
+ request: Request,
335
+ env: TEnv,
336
+ continuation: ActionContinuation,
337
+ ): Promise<ActionContinuation> {
338
+ if (continuation.returnValue.ok) return continuation;
339
+ const errorBoundary =
340
+ (await ctx.router.matchError(
341
+ request,
342
+ { env },
343
+ continuation.actionContext.actionResult,
344
+ "route",
345
+ )) ?? undefined;
346
+ return { ...continuation, errorBoundary };
347
+ }
348
+
326
349
  /**
327
350
  * Phase 2: Revalidate after action.
328
351
  *
@@ -513,7 +536,7 @@ function renderActionBoundaryResponse<TEnv>(
513
536
 
514
537
  const errorPayload: RscPayload = {
515
538
  metadata: {
516
- pathname: url.pathname,
539
+ ...payloadLocation(reqCtx, url),
517
540
  // routerId exposed for the frontend (current app identity); see
518
541
  // rsc-rendering.ts partial branch.
519
542
  routerId: ctx.router.id,
@@ -576,7 +599,7 @@ function renderRevalidationResponse<TEnv>(
576
599
 
577
600
  const payload: RscPayload = {
578
601
  metadata: {
579
- pathname: url.pathname,
602
+ ...payloadLocation(reqCtx, url),
580
603
  // routerId exposed for the frontend (current app identity); see
581
604
  // rsc-rendering.ts partial branch.
582
605
  routerId: ctx.router.id,
@@ -67,6 +67,8 @@ export const PPR_REPLAY_BYPASS_REASONS = [
67
67
  "no-segment-snapshot",
68
68
  "snapshot-miss",
69
69
  "explicit-cache-hit",
70
+ // A rewritten request whose rewrite chose no `.shell()`.
71
+ "rewritten",
70
72
  ] as const;
71
73
 
72
74
  /** One of {@link PPR_REPLAY_BYPASS_REASONS}. */
@@ -126,6 +128,26 @@ export function partitionShellKey(key: string, partition: string): string {
126
128
  return `${key}|${encodeURIComponent(partition)}`;
127
129
  }
128
130
 
131
+ /**
132
+ * The shell key of a `rewrite(...).shell("visitor")` request: the visitor's
133
+ * URL, partitioned by `rw:` + the target's path and search (both searches
134
+ * filtered and sorted, shellSearchSeed). The visitor's URL keeps two public
135
+ * URLs of one target apart; the target keeps a conditional rewrite (an A/B
136
+ * arm, a lock) apart from the URL's own shell. The route's own partition
137
+ * composes after it.
138
+ */
139
+ export function visitorShellKey(
140
+ routerId: string,
141
+ visitor: URL,
142
+ target: URL,
143
+ filter?: SearchParamsFilter,
144
+ ): string {
145
+ return partitionShellKey(
146
+ buildShellKey(routerId, visitor, filter),
147
+ `rw:${target.pathname}${shellSearchSeed(target, filter)}`,
148
+ );
149
+ }
150
+
129
151
  /**
130
152
  * The navigation-only entry beside a document shell key (a cold partial
131
153
  * request's capture, rsc-rendering.ts). A suffix after the (encoded)
@@ -186,9 +208,10 @@ export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
186
208
  * (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
187
209
  * result): `identity` (a request-scoped read: cookies(), headers(), the
188
210
  * theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
189
- * `loader` (a `{ ssr: false }` loader rejected, or settled with
190
- * redirect()/notFound()), `no-record` (no doc record: the route's cache()
191
- * refused the write, or the handler output failed), `handles` (a pushed
211
+ * `loader` (a `{ ssr: false }` loader rejected, settled with
212
+ * redirect()/notFound(), or a promise it pushed rejected),
213
+ * `no-record` (no doc record: the route's cache() refused the write, or the
214
+ * handler output failed), `handles` (a pushed
192
215
  * handle value did not finish encoding), `size` (over the store's value
193
216
  * limit), `record-expired` (the route cache() record the capture wrote ran
194
217
  * out first), `invalidated` (a tag of the shell was invalidated after the
@@ -508,6 +508,11 @@ function refuse(
508
508
  return "refused";
509
509
  }
510
510
 
511
+ /** A bake-lane loader's promise push, as a refusal names it. */
512
+ function describeLoaderPromisePush(push: LoaderPromisePush): string {
513
+ return `the promise the loader "${push.loaderId}" pushed to the handle "${push.handleName}"`;
514
+ }
515
+
511
516
  /** The no-shell cause for handler output that missed the capture deadline. */
512
517
  const HANDLER_OUTPUT_TIMEOUT_REASON: string =
513
518
  "the handler output (a promise it passes or pushes, or an async server " +
@@ -1063,6 +1068,13 @@ export interface ShellCaptureDescriptor {
1063
1068
  debugSink?: (event: ShellCaptureDebugEvent) => void;
1064
1069
  /** Store the snapshot for navigation replay, never the captured HTML prelude. */
1065
1070
  navigationOnly?: true;
1071
+ /**
1072
+ * Set for a shell keyed by the visitor's URL (`rewrite(...).shell("visitor")`,
1073
+ * visitorShellKey): the capture renders the visitor's location
1074
+ * (`RequestContext._shellCaptureForVisitor`) and stores it as
1075
+ * ShellCacheEntry.pathname.
1076
+ */
1077
+ pathname?: string;
1066
1078
  /**
1067
1079
  * lastStoredCaptureSeq(key) when a document request read the store and
1068
1080
  * missed. The request schedules its capture only after rendering the MISS,
@@ -1660,8 +1672,9 @@ export type CaptureRecordOutcome =
1660
1672
  * component showed one value in the prelude and another on every HIT.
1661
1673
  *
1662
1674
  * Order, all bounded by the one capture deadline (`ppr.captureTimeout`):
1663
- * top-level handle promises, the handler pushes' nested promises, and the
1664
- * bake-lane loader containers settle first (the record encodes the handles
1675
+ * top-level handle promises (a bake-lane loader's pushed promises among
1676
+ * them, #1182), the handler pushes' nested promises, and the bake-lane
1677
+ * loader containers settle first (the record encodes the handles
1665
1678
  * a HIT restores, and its handle encode has its own 5s timeout that must
1666
1679
  * never be the one that fires); then the capture's onResponse callbacks fire
1667
1680
  * with a synthetic 200 (the implicit doc scope's cacheRoute, an explicit
@@ -1704,7 +1717,33 @@ export async function settleCaptureRecord(
1704
1717
  if (!settled.done) {
1705
1718
  if (refuseOnCaptureGuard(capture.key, derivedCtx))
1706
1719
  return { kind: "refused", refusal: guardRefusal(derivedCtx) };
1707
- return { kind: "timeout", reason: HANDLER_OUTPUT_TIMEOUT_REASON };
1720
+ const pending = derivation.loaderPromisePushes.find(
1721
+ (p) => p.state === "pending",
1722
+ );
1723
+ return {
1724
+ kind: "timeout",
1725
+ reason: pending
1726
+ ? `${describeLoaderPromisePush(pending)} did not settle within ppr.captureTimeout`
1727
+ : HANDLER_OUTPUT_TIMEOUT_REASON,
1728
+ };
1729
+ }
1730
+ // A rejected value must not bake, and the record's encode would fail on
1731
+ // it anyway: refuse before the write starts. A guard trip in that promise
1732
+ // names the read instead.
1733
+ const rejected = derivation.loaderPromisePushes.find(
1734
+ (p) => p.state === "rejected",
1735
+ );
1736
+ if (rejected) {
1737
+ if (refuseOnCaptureGuard(capture.key, derivedCtx))
1738
+ return { kind: "refused", refusal: guardRefusal(derivedCtx) };
1739
+ warnCaptureRefusedOnce(
1740
+ capture.key,
1741
+ `${describeLoaderPromisePush(rejected)} rejected during capture; a ` +
1742
+ "failed value must not bake into the shared shell. Catch it and push " +
1743
+ "a fallback, or push it from a loader without ssr: false.",
1744
+ PPR_LANE_HINT,
1745
+ );
1746
+ return { kind: "refused", refusal: "loader" };
1708
1747
  }
1709
1748
 
1710
1749
  const callbacks = derivedCtx._onResponseCallbacks?.splice(0) ?? [];
@@ -1878,6 +1917,9 @@ async function attemptCapture(
1878
1917
  descriptor.navigationOnly ? { request, url } : undefined,
1879
1918
  );
1880
1919
  const { derivedCtx, freshHandleStore } = derivation;
1920
+ if (descriptor.pathname !== undefined) {
1921
+ derivedCtx._shellCaptureForVisitor = true;
1922
+ }
1881
1923
  // The capture generation starts before matching or any snapshot read
1882
1924
  // (ShellCacheEntry.createdAt): a tag invalidated after it wins.
1883
1925
  const captureStartedAt = Date.now();
@@ -1996,6 +2038,20 @@ export interface CaptureContextDerivation {
1996
2038
  * record encodes its handles, so that encode never races a pending value.
1997
2039
  */
1998
2040
  handlerPushesSettled: () => Promise<void>;
2041
+ /**
2042
+ * Every promise a bake-lane loader body pushed (#1182), in push order.
2043
+ * The record-first step waits for them (they are in the recorded cut) and
2044
+ * names the loader and the handle of one that rejects or misses the
2045
+ * deadline.
2046
+ */
2047
+ loaderPromisePushes: readonly LoaderPromisePush[];
2048
+ }
2049
+
2050
+ /** A promise a bake-lane loader pushed at capture, owned by that loader. */
2051
+ export interface LoaderPromisePush {
2052
+ loaderId: string;
2053
+ handleName: string;
2054
+ state: "pending" | "fulfilled" | "rejected";
1999
2055
  }
2000
2056
 
2001
2057
  /**
@@ -2031,30 +2087,35 @@ export function deriveShellCaptureContext(
2031
2087
  // masked), their pushes are in the captured HTML (<head> title/meta,
2032
2088
  // useHandle echoes), and a HIT does not run a promise-free bake-lane
2033
2089
  // loader (loader-cache.ts), so the stored record carries them: each
2034
- // settled, thenable-free push from a loader body keeps that loader as
2035
- // `owner` (HandleStore.push), the record stores it
2090
+ // push from a loader body whose value, once settled, is thenable-free
2091
+ // keeps that loader as `owner` (HandleStore.push), the record stores it
2036
2092
  // (CachedEntryData.handleOwners), and a replay that serves the loader
2037
- // from its pin restores it through pushRestored (restoreHandles). Those
2038
- // copies stand: a promise-carrying bake-lane loader that runs on the
2039
- // replay reads the store, so its settled pushes (and those replayed
2040
- // inside its body) are dropped and only its thenable ones are added. A
2041
- // loader the replay does not serve from a pin is a hole: one the route
2042
- // also runs on the live lane, or one whose pin the entry lost. Its run
2043
- // replaces its copies (pushPlaceholder, #936), those of the
2044
- // dependencies it awaits included (they are credited to it). Nested
2045
- // thenables stay masked (the promise shape is the liveness
2046
- // declaration, mask-nested.ts). A deferred (thenable) push,
2047
- // one with masked nested promises, or one outside a loader body (a
2048
- // loader-cache replay names its loader: pushReplayed below) stays
2049
- // tagged (HandleStore.push loaderPush) so the
2050
- // handle encode cannot stall on a never-resolving mask. A tagged push
2051
- // is out of the record (captureHandles) and out of the capture's own
2052
- // render (resolvedHandleStream `recordedOnly`, issue #1035): the
2053
- // prelude is rendered from what the record keeps, which is what a HIT
2054
- // hydrates with. The loader records then carry `runs: 1` so a HIT runs
2055
- // those bodies for them, and what they push reaches the client after
2093
+ // from its pin restores it through pushRestored (restoreHandles). A
2094
+ // pushed promise (push(promise), .defer()) is judged by its settled
2095
+ // value, as push(await promise) would be (#1182): the record-first step
2096
+ // waits for it under the capture deadline, and one that rejects or
2097
+ // misses the deadline refuses the capture. Those copies stand: a
2098
+ // promise-carrying bake-lane loader that runs on the replay reads the
2099
+ // store, so its pushes that settle thenable-free (and those replayed
2100
+ // inside its body) are dropped and only the ones holding a promise are
2101
+ // added. A loader the replay does not serve from a pin is a hole: one
2102
+ // the route also runs on the live lane, or one whose pin the entry
2103
+ // lost. Its run replaces its copies (pushPlaceholder, #936), those of
2104
+ // the dependencies it awaits included (they are credited to it). Nested
2105
+ // thenables stay masked (the promise shape is the liveness declaration,
2106
+ // mask-nested.ts). A push whose settled value holds a masked nested
2107
+ // promise, one credited to a live-lane body, or one no body or replay
2108
+ // names (a loader-cache replay names its loader: pushReplayed below)
2109
+ // stays tagged (HandleStore.push loaderPush) so the handle encode cannot
2110
+ // stall on a never-resolving mask. A tagged push is out of the record
2111
+ // (captureHandles) and out of the capture's own render
2112
+ // (resolvedHandleStream `recordedOnly`, issue #1035): the prelude is
2113
+ // rendered from what the record keeps, which is what a HIT hydrates
2114
+ // with. The loader records then carry `runs: 1` so a HIT runs those
2115
+ // bodies for them, and what they push reaches the client after
2056
2116
  // hydration (HandleStore.freezeDocumentSnapshot).
2057
2117
  const handlerPushSettles: Promise<void>[] = [];
2118
+ const loaderPromisePushes: LoaderPromisePush[] = [];
2058
2119
  // A replay (a loader-cache HIT, loader-cache.ts replayLoaderHandles, or a
2059
2120
  // route cache() record's owned values, restoreHandles) re-pushes a loader's
2060
2121
  // recorded values through pushReplayed or pushPlaceholder, which call
@@ -2108,39 +2169,62 @@ export function deriveShellCaptureContext(
2108
2169
  if (report.thenable) maskedNestedThenable = true;
2109
2170
  return masked;
2110
2171
  };
2172
+ // The loader the push is credited to (see the funnel comment above). A
2173
+ // push under a live-lane loader's body, at any depth, is that hole's
2174
+ // output: its live run replaces it on a HIT and a navigation. Otherwise
2175
+ // the first loader around the push that the route registers decides: a
2176
+ // bake-lane one keeps the push under the innermost body, and a
2177
+ // dependency on neither lane is walked past. A replay of a hole's push
2178
+ // is that hole's, unless a live-lane body around it takes it first.
2179
+ const live = liveLaneOwner();
2180
+ const bodyLoaderId = live ?? getCurrentLoaderBodyId() ?? replayOwner;
2111
2181
  let masked: unknown;
2112
- let loaderPush = true;
2113
2182
  let owner: string | undefined;
2114
- if (isThenable(value)) {
2115
- // Deferred (thenable) loader pushes always keep the tag — the
2116
- // carve-out below is for settled values only. Promise.resolve first: a
2117
- // loader cache() entry replays a deferred push as the thenable Flight
2118
- // decoded it to, whose then() returns nothing, and the slot was
2119
- // `undefined` (the empty element of issue #1035).
2120
- masked = Promise.resolve(value).then(mask);
2121
- } else {
2183
+ if (!isThenable(value)) {
2122
2184
  masked = mask(value);
2123
- if (!maskedNestedThenable) {
2124
- // A settled, thenable-free push from a loader body is shell
2125
- // material (see the funnel comment above): only bake-lane loaders
2126
- // and the loaders they await run at capture, and a HIT does not run
2127
- // a promise-free bake-lane loader (loader-cache.ts), so the record
2128
- // keeps the push under the loader that made it. A push under a
2129
- // live-lane loader's body, at any depth, is that hole's output: it
2130
- // is credited to the hole, whose live run replaces it on a HIT and
2131
- // a navigation. The first loader around the push that the route
2132
- // registers decides: a bake-lane one keeps the push under the
2133
- // innermost body, and a dependency on neither lane is walked past. A
2134
- // replay of a hole's push is that hole's, unless a live-lane body
2135
- // around it takes it first.
2136
- const bodyLoaderId =
2137
- liveLaneOwner() ?? getCurrentLoaderBodyId() ?? replayOwner;
2138
- loaderPush = bodyLoaderId === undefined;
2139
- if (!loaderPush) owner = bodyLoaderId;
2140
- }
2185
+ if (!maskedNestedThenable) owner = bodyLoaderId;
2186
+ } else if (live === undefined && bodyLoaderId !== undefined) {
2187
+ // Owned now, judged by its settled value (funnel comment above).
2188
+ // Promise.resolve first: a loader cache() entry replays a deferred
2189
+ // push as the thenable Flight decoded it to, whose then() returns
2190
+ // nothing (issue #1035). The retag runs inside the slot's own then(),
2191
+ // so it lands before the wait on it (settleCaptureRecord) resumes and
2192
+ // the record encodes. The state handler is attached now, which also
2193
+ // marks a rejection as handled.
2194
+ const slot: Promise<unknown> = Promise.resolve(value).then((settled) => {
2195
+ const judged = mask(settled);
2196
+ if (maskedNestedThenable) {
2197
+ freshHandleStore.unrecordPush(handleName, segmentId, slot);
2198
+ derivedCtx._shellCaptureUnrecordedLoaderPush = true;
2199
+ }
2200
+ return judged;
2201
+ });
2202
+ const push: LoaderPromisePush = {
2203
+ loaderId: bodyLoaderId,
2204
+ handleName,
2205
+ state: "pending",
2206
+ };
2207
+ slot.then(
2208
+ () => {
2209
+ push.state = "fulfilled";
2210
+ },
2211
+ () => {
2212
+ push.state = "rejected";
2213
+ },
2214
+ );
2215
+ loaderPromisePushes.push(push);
2216
+ masked = slot;
2217
+ owner = bodyLoaderId;
2218
+ } else {
2219
+ // A hole's promise, or one no body or replay names: tagged. Nothing
2220
+ // awaits the slot at capture, so its rejection is handled here.
2221
+ const tagged = Promise.resolve(value).then(mask);
2222
+ tagged.catch(() => {});
2223
+ masked = tagged;
2141
2224
  }
2142
2225
  // A push the record cannot keep reaches a HIT only from a live run of
2143
2226
  // its loader, so the loader records ask for one (runs: 1).
2227
+ const loaderPush = owner === undefined;
2144
2228
  if (loaderPush) derivedCtx._shellCaptureUnrecordedLoaderPush = true;
2145
2229
  rawCapturePush(handleName, segmentId, masked, loaderPush, owner);
2146
2230
  };
@@ -2261,6 +2345,7 @@ export function deriveShellCaptureContext(
2261
2345
  freshHandleStore,
2262
2346
  handlerPushesSettled: () =>
2263
2347
  Promise.all(handlerPushSettles).then(() => undefined),
2348
+ loaderPromisePushes,
2264
2349
  };
2265
2350
  }
2266
2351
 
@@ -2647,6 +2732,7 @@ async function captureAndStoreShell(
2647
2732
  // default, #971). The serve tail replays it so the resume tree
2648
2733
  // matches the frozen prelude — see ShellCacheEntry.initialTheme.
2649
2734
  initialTheme: payloadInitialTheme(reqCtx),
2735
+ pathname: capture.pathname,
2650
2736
  snapshot,
2651
2737
  prunedRecords: snapshot.length > 0 ? prunedRecords : undefined,
2652
2738
  // The canonical doc segment record's key, published by the doc
package/src/rsc/types.ts CHANGED
@@ -18,6 +18,11 @@ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
18
18
  export interface RscPayload {
19
19
  metadata?: {
20
20
  pathname: string;
21
+ /**
22
+ * Path and search of the URL the route rendered for, when middleware
23
+ * rewrote the request there (full-payload.ts payloadLocation).
24
+ */
25
+ rewriteTarget?: string;
21
26
  segments: ResolvedSegment[];
22
27
  /** Router instance ID. When this changes between navigations, the client
23
28
  * discards cached segments and does a full tree replacement (app switch). */
@@ -364,15 +369,6 @@ export interface CreateRSCHandlerOptions<
364
369
  */
365
370
  cache?: HandlerCacheConfig | ((env: TEnv) => HandlerCacheConfig);
366
371
 
367
- /**
368
- * Version override for this handler. Same meaning as
369
- * `createRouter({ version })`, which it takes precedence over: this exact
370
- * value is used for both of the router's versions.
371
- *
372
- * @default createRouter({ version }), else the router's build versions
373
- */
374
- version?: string;
375
-
376
372
  /**
377
373
  * Nonce provider for Content Security Policy (CSP).
378
374
  *