@rangojs/router 0.24.4 → 0.25.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 (267) 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-shell-frame.d.ts +2 -0
  21. package/dist/types/cache/types.d.ts +8 -0
  22. package/dist/types/client-urls/client-urls.d.ts +6 -2
  23. package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
  24. package/dist/types/client-urls/server-projection.d.ts +9 -5
  25. package/dist/types/client-urls/types.d.ts +29 -16
  26. package/dist/types/client.d.ts +2 -2
  27. package/dist/types/client.rsc.d.ts +2 -2
  28. package/dist/types/decode-loader-results.d.ts +28 -7
  29. package/dist/types/href-client.d.ts +3 -1
  30. package/dist/types/index.rsc.d.ts +1 -1
  31. package/dist/types/loader-redirect.d.ts +2 -3
  32. package/dist/types/outlet-context.d.ts +7 -6
  33. package/dist/types/prefetch-markers.d.ts +14 -5
  34. package/dist/types/route-content-wrapper.d.ts +17 -14
  35. package/dist/types/route-definition/helpers-types.d.ts +7 -2
  36. package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
  37. package/dist/types/route-definition/rewrite.d.ts +48 -5
  38. package/dist/types/route-types.d.ts +2 -2
  39. package/dist/types/router/handler-context.d.ts +14 -2
  40. package/dist/types/router/intercept-resolution.d.ts +19 -46
  41. package/dist/types/router/lazy-includes.d.ts +7 -0
  42. package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
  43. package/dist/types/router/middleware.d.ts +0 -16
  44. package/dist/types/router/navigation-snapshot.d.ts +2 -0
  45. package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
  46. package/dist/types/router/revalidation.d.ts +8 -1
  47. package/dist/types/router/router-context.d.ts +4 -16
  48. package/dist/types/router/router-interfaces.d.ts +0 -6
  49. package/dist/types/router/router-options.d.ts +4 -7
  50. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
  51. package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
  52. package/dist/types/router/segment-resolution.d.ts +1 -1
  53. package/dist/types/router/segment-wrappers.d.ts +3 -5
  54. package/dist/types/router/substitute-pattern-params.d.ts +3 -2
  55. package/dist/types/router/tracing.d.ts +2 -2
  56. package/dist/types/rsc/full-payload.d.ts +14 -2
  57. package/dist/types/rsc/helpers.d.ts +8 -0
  58. package/dist/types/rsc/manifest-init.d.ts +20 -0
  59. package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
  60. package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
  61. package/dist/types/rsc/rewrite-passes.d.ts +12 -2
  62. package/dist/types/rsc/server-action.d.ts +6 -0
  63. package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
  64. package/dist/types/rsc/shell-capture.d.ts +7 -0
  65. package/dist/types/rsc/types.d.ts +5 -0
  66. package/dist/types/segment-system.d.ts +15 -0
  67. package/dist/types/server/context.d.ts +64 -4
  68. package/dist/types/server/request-context.d.ts +41 -4
  69. package/dist/types/server/request-headers.d.ts +12 -0
  70. package/dist/types/server/rewrite-trail.d.ts +8 -1
  71. package/dist/types/testing/e2e/index.d.ts +1 -1
  72. package/dist/types/testing/flight.entry.d.ts +1 -1
  73. package/dist/types/testing/index.d.ts +1 -1
  74. package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
  75. package/dist/types/testing/render-route.d.ts +21 -11
  76. package/dist/types/testing/run-loader.d.ts +7 -0
  77. package/dist/types/testing/run-middleware.d.ts +7 -0
  78. package/dist/types/testing/run-revalidate.d.ts +5 -4
  79. package/dist/types/testing/serve-shell-request.d.ts +41 -4
  80. package/dist/types/testing/shell-status.d.ts +17 -0
  81. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  82. package/dist/types/testing/vitest.d.ts +10 -1
  83. package/dist/types/thenable-status.d.ts +0 -6
  84. package/dist/types/types/handler-context.d.ts +12 -1
  85. package/dist/types/types/loader-types.d.ts +4 -3
  86. package/dist/types/types/route-entry.d.ts +2 -1
  87. package/dist/types/urls/path-helper-types.d.ts +4 -2
  88. package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
  89. package/dist/types/vite/discovery/state.d.ts +20 -0
  90. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
  91. package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
  92. package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
  93. package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
  94. package/dist/vite/index.js +6816 -6547
  95. package/package.json +1 -1
  96. package/skills/app-structure/SKILL.md +255 -0
  97. package/skills/app-structure/content-site.md +333 -0
  98. package/skills/app-structure/e-commerce.md +435 -0
  99. package/skills/app-structure/large-app.md +885 -0
  100. package/skills/app-structure/multi-app.md +280 -0
  101. package/skills/breadcrumbs/SKILL.md +6 -1
  102. package/skills/caching/SKILL.md +3 -1
  103. package/skills/catalog.json +12 -0
  104. package/skills/client-urls/SKILL.md +185 -32
  105. package/skills/composability/SKILL.md +10 -8
  106. package/skills/handler-use/SKILL.md +48 -9
  107. package/skills/hooks/handle-and-actions.md +104 -0
  108. package/skills/hooks/navigation.md +24 -10
  109. package/skills/hooks/urls.md +1 -1
  110. package/skills/i18n/SKILL.md +2 -1
  111. package/skills/intercept/SKILL.md +60 -29
  112. package/skills/layout/SKILL.md +6 -3
  113. package/skills/links/SKILL.md +151 -17
  114. package/skills/loader/SKILL.md +32 -10
  115. package/skills/middleware/SKILL.md +111 -15
  116. package/skills/observability/SKILL.md +11 -0
  117. package/skills/parallel/SKILL.md +5 -5
  118. package/skills/ppr/SKILL.md +43 -8
  119. package/skills/prerender/SKILL.md +1 -2
  120. package/skills/rango/SKILL.md +3 -0
  121. package/skills/response-routes/SKILL.md +4 -1
  122. package/skills/scripts/SKILL.md +12 -1
  123. package/skills/server-actions/SKILL.md +60 -20
  124. package/skills/testing/SKILL.md +1 -0
  125. package/skills/testing/client-components.md +13 -10
  126. package/skills/testing/loader.md +25 -24
  127. package/skills/testing/middleware.md +11 -11
  128. package/skills/testing/response-routes.md +4 -1
  129. package/skills/testing/serve-shell-request.md +85 -21
  130. package/skills/ux-architecture/SKILL.md +980 -0
  131. package/skills/view-transitions/SKILL.md +43 -10
  132. package/src/__augment-tests__/augmented.check.ts +28 -0
  133. package/src/browser/event-controller.ts +44 -18
  134. package/src/browser/intercept-utils.ts +3 -2
  135. package/src/browser/merge-segment-loaders.ts +0 -85
  136. package/src/browser/navigation-bridge.ts +112 -84
  137. package/src/browser/navigation-client.ts +20 -15
  138. package/src/browser/navigation-store.ts +4 -2
  139. package/src/browser/navigation-transaction.ts +9 -8
  140. package/src/browser/partial-update.ts +90 -32
  141. package/src/browser/prefetch/cache.ts +8 -0
  142. package/src/browser/prefetch/fetch.ts +13 -3
  143. package/src/browser/rango-marker.ts +13 -0
  144. package/src/browser/react/NavigationProvider.tsx +25 -7
  145. package/src/browser/react/filter-segment-order.ts +13 -0
  146. package/src/browser/react/use-action.ts +9 -15
  147. package/src/browser/react/use-handle.ts +63 -71
  148. package/src/browser/react/use-href.tsx +24 -5
  149. package/src/browser/react/use-link-status.ts +1 -1
  150. package/src/browser/react/use-navigation.ts +30 -40
  151. package/src/browser/react/use-router.ts +37 -6
  152. package/src/browser/react/use-segments.ts +8 -12
  153. package/src/browser/response-adapter.ts +11 -5
  154. package/src/browser/rsc-router.tsx +3 -1
  155. package/src/browser/scroll-restoration.ts +34 -20
  156. package/src/browser/segment-reconciler.ts +11 -29
  157. package/src/browser/server-action-bridge.ts +52 -16
  158. package/src/browser/types.ts +40 -8
  159. package/src/build/generate-manifest.ts +12 -1
  160. package/src/build/route-types/codegen.ts +6 -2
  161. package/src/build/route-types/per-module-writer.ts +94 -61
  162. package/src/cache/cache-runtime.ts +9 -0
  163. package/src/cache/cache-scope.ts +2 -1
  164. package/src/cache/cf/cf-cache-store.ts +2 -0
  165. package/src/cache/cf/cf-shell-frame.ts +3 -0
  166. package/src/cache/types.ts +8 -0
  167. package/src/cache/vercel/vercel-cache-store.ts +6 -1
  168. package/src/client-urls/client-urls.ts +202 -153
  169. package/src/client-urls/revalidate-registry.ts +12 -3
  170. package/src/client-urls/server-projection.ts +137 -66
  171. package/src/client-urls/types.ts +30 -15
  172. package/src/client.rsc.tsx +11 -1
  173. package/src/client.tsx +9 -1
  174. package/src/decode-loader-results.ts +64 -31
  175. package/src/href-client.ts +3 -1
  176. package/src/index.rsc.ts +1 -0
  177. package/src/index.ts +12 -4
  178. package/src/loader-redirect.tsx +2 -3
  179. package/src/outlet-context.ts +7 -6
  180. package/src/prefetch-markers.ts +15 -5
  181. package/src/reverse.ts +3 -3
  182. package/src/route-content-wrapper.tsx +19 -17
  183. package/src/route-definition/dsl-helpers.ts +27 -7
  184. package/src/route-definition/helpers-types.ts +7 -2
  185. package/src/route-definition/resolve-handler-use.ts +23 -10
  186. package/src/route-definition/rewrite.ts +86 -19
  187. package/src/route-types.ts +1 -2
  188. package/src/router/find-match.ts +37 -1
  189. package/src/router/handler-context.ts +85 -40
  190. package/src/router/intercept-resolution.ts +161 -307
  191. package/src/router/lazy-includes.ts +43 -27
  192. package/src/router/loader-resolution.ts +21 -1
  193. package/src/router/manifest.ts +6 -8
  194. package/src/router/match-api.ts +11 -1
  195. package/src/router/match-middleware/background-revalidation.ts +2 -4
  196. package/src/router/match-middleware/cache-lookup.ts +16 -20
  197. package/src/router/match-middleware/intercept-resolution.ts +41 -60
  198. package/src/router/match-result.ts +31 -2
  199. package/src/router/middleware.ts +6 -147
  200. package/src/router/navigation-snapshot.ts +38 -2
  201. package/src/router/prefetch-cache-ttl.ts +10 -18
  202. package/src/router/prerender-match.ts +2 -1
  203. package/src/router/revalidation.ts +24 -3
  204. package/src/router/router-context.ts +5 -18
  205. package/src/router/router-interfaces.ts +0 -7
  206. package/src/router/router-options.ts +4 -7
  207. package/src/router/segment-resolution/fresh.ts +6 -2
  208. package/src/router/segment-resolution/helpers.ts +7 -1
  209. package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
  210. package/src/router/segment-resolution/revalidation.ts +75 -35
  211. package/src/router/segment-resolution.ts +0 -1
  212. package/src/router/segment-wrappers.ts +8 -13
  213. package/src/router/substitute-pattern-params.ts +3 -2
  214. package/src/router/tracing.ts +2 -2
  215. package/src/router.ts +2 -9
  216. package/src/rsc/full-payload.ts +26 -4
  217. package/src/rsc/handler.ts +83 -41
  218. package/src/rsc/helpers.ts +13 -0
  219. package/src/rsc/manifest-init.ts +56 -3
  220. package/src/rsc/prefetch-render-error.ts +38 -0
  221. package/src/rsc/progressive-enhancement.ts +161 -211
  222. package/src/rsc/rewrite-passes.ts +56 -9
  223. package/src/rsc/rsc-rendering.ts +79 -23
  224. package/src/rsc/server-action.ts +25 -2
  225. package/src/rsc/shell-capture-constants.ts +22 -0
  226. package/src/rsc/shell-capture.ts +11 -0
  227. package/src/rsc/types.ts +5 -0
  228. package/src/segment-system.tsx +125 -119
  229. package/src/server/context.ts +96 -5
  230. package/src/server/handle-store.ts +3 -2
  231. package/src/server/request-context.ts +42 -3
  232. package/src/server/request-headers.ts +32 -0
  233. package/src/server/rewrite-trail.ts +55 -9
  234. package/src/ssr/ssr-root.tsx +6 -10
  235. package/src/testing/dispatch.ts +4 -5
  236. package/src/testing/e2e/index.ts +1 -0
  237. package/src/testing/flight.entry.ts +1 -0
  238. package/src/testing/index.ts +1 -0
  239. package/src/testing/internal/client-urls-projection.ts +31 -11
  240. package/src/testing/render-route.tsx +83 -40
  241. package/src/testing/run-loader.ts +26 -2
  242. package/src/testing/run-middleware.ts +10 -0
  243. package/src/testing/run-revalidate.ts +13 -6
  244. package/src/testing/serve-shell-request.ts +77 -4
  245. package/src/testing/shell-status.ts +40 -0
  246. package/src/testing/vitest-stubs/version.ts +1 -0
  247. package/src/testing/vitest.ts +74 -7
  248. package/src/thenable-status.ts +0 -11
  249. package/src/types/handler-context.ts +12 -1
  250. package/src/types/loader-types.ts +4 -3
  251. package/src/types/route-entry.ts +2 -1
  252. package/src/urls/include-helper.ts +38 -11
  253. package/src/urls/path-helper-types.ts +4 -2
  254. package/src/vite/discovery/client-urls-projection.ts +205 -16
  255. package/src/vite/discovery/discover-routers.ts +2 -2
  256. package/src/vite/discovery/prerender-collection.ts +2 -6
  257. package/src/vite/discovery/state.ts +26 -0
  258. package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
  259. package/src/vite/plugins/expose-internal-ids.ts +46 -40
  260. package/src/vite/plugins/hoist/source.ts +8 -7
  261. package/src/vite/plugins/transition-when-hoist.ts +1 -0
  262. package/src/vite/plugins/version-plugin.ts +7 -0
  263. package/src/vite/plugins/version.d.ts +7 -0
  264. package/src/vite/plugins/virtual-entries.ts +20 -0
  265. package/src/vite/rango.ts +4 -1
  266. package/src/vite/router-discovery.ts +39 -7
  267. package/src/vite/utils/prerender-utils.ts +17 -0
@@ -21,7 +21,11 @@ import {
21
21
  rebaseRequestContext,
22
22
  type RequestContext,
23
23
  } from "../server/request-context.js";
24
- import { recordRewriteHop } from "../server/rewrite-trail.js";
24
+ import { bodylessRequest } from "../server/request-headers.js";
25
+ import {
26
+ recordRewriteHop,
27
+ recordRewriteShell,
28
+ } from "../server/rewrite-trail.js";
25
29
 
26
30
  /** Response header naming the route path a rewritten request rendered. */
27
31
  export const REWRITE_HEADER = "x-rango-rewrite";
@@ -45,11 +49,9 @@ export function resolveRewriteTarget(
45
49
  const path =
46
50
  "path" in target
47
51
  ? prefixBasename(target.path, basename)
48
- : // `{}`, not undefined: a route with params then throws for the
49
- // missing ones instead of resolving to its raw pattern.
50
- createReverseFunction(routeMap)(
52
+ : createReverseFunction(routeMap)(
51
53
  target.name,
52
- target.params ?? {},
54
+ target.params,
53
55
  target.search,
54
56
  );
55
57
  const resolved = stripInternalParams(new URL(path, base.origin));
@@ -94,11 +96,38 @@ export interface RewritePass {
94
96
  hops: number;
95
97
  }
96
98
 
99
+ let lateLockReported = false;
100
+
101
+ /**
102
+ * `.disallowMutations()` thrown after the action ran cannot refuse it. Dev
103
+ * warns on every throw; production logs one error per isolate, so a lock
104
+ * that never refused anything is not silent there.
105
+ */
106
+ function reportLateLock(rewrite: Rewrite): void {
107
+ const message = `[rango] ${describeRewriteTarget(rewrite.target)}.disallowMutations() came after the server action or form post ran, so it cannot refuse it: route middleware, and global middleware on a later rewritten pass, run only after the action. The render moves to the target. To refuse mutations, throw it from global middleware (router.use()) matched on the URL the form posts to.`;
108
+ if (process.env.NODE_ENV !== "production") {
109
+ console.warn(message);
110
+ } else if (!lateLockReported) {
111
+ lateLockReported = true;
112
+ console.error(message);
113
+ }
114
+ }
115
+
97
116
  /**
98
117
  * Move the request context to `rewrite`'s target for the next pass: check
99
118
  * that this request can be rewritten, resolve the target, count the hop,
100
- * then rebase the context. Throws the site error for a mutation or a loader
101
- * fetch, a resolution error, and RewriteLoopError.
119
+ * then rebase the context. Throws the site error for a loader fetch, a
120
+ * refused mutation or one whose body middleware read before its action ran,
121
+ * a resolution error, and RewriteLoopError.
122
+ *
123
+ * A mutation moves too (#1145). Before its action ran (a rewrite from global
124
+ * middleware) it moves with its body, and the target's pass runs the action
125
+ * after the target's global middleware, as a direct post there would. After
126
+ * it ran (route middleware wraps only the re-render), the target's pass
127
+ * re-renders from `reqCtx._actionRan`. `disallowMutations()` refuses the
128
+ * first, so a lock never runs a mutation; the second is already committed,
129
+ * and refusing it reports an error for a write that happened, so the caller
130
+ * retries it.
102
131
  */
103
132
  export function applyRewrite(
104
133
  rewrite: Rewrite,
@@ -113,12 +142,30 @@ export function applyRewrite(
113
142
  throw rewriteNotSupported(rewrite, "loader-fetch");
114
143
  }
115
144
  if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
116
- throw rewriteNotSupported(rewrite, "mutation");
145
+ if (!reqCtx._actionRan) {
146
+ if (rewrite.mutationsDisallowed) {
147
+ throw rewriteNotSupported(rewrite, "mutation");
148
+ }
149
+ // The target's pass hands the body to the action: one middleware read
150
+ // would run it with no arguments.
151
+ if (passRequest.bodyUsed) throw rewriteNotSupported(rewrite, "body-read");
152
+ } else if (rewrite.mutationsDisallowed) {
153
+ reportLateLock(rewrite);
154
+ }
117
155
  }
118
156
  const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
119
157
  const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
158
+ recordRewriteShell(reqCtx, rewrite.shellAudience);
120
159
  const url = rewriteTransportUrl(target, reqCtx.originalUrl);
121
- const request = new Request(url, passRequest);
160
+ // A mutation keeps its body: one whose action has not run yet, and a no-JS
161
+ // form post, since the PE handler reads a clone, so a form-field check
162
+ // reads it on every pass. A JS action that ran consumed it
163
+ // (`new Request(url, used)` throws): the same request without it or its
164
+ // content-type, so a body check gated on the content-type passes over this
165
+ // pass, which never runs the action.
166
+ const request = passRequest.bodyUsed
167
+ ? bodylessRequest(url, passRequest, passRequest.method)
168
+ : new Request(url, passRequest);
122
169
  rebaseRequestContext(reqCtx, request, url);
123
170
  return { request, url, target, hops };
124
171
  }
@@ -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)
@@ -1063,6 +1063,13 @@ export interface ShellCaptureDescriptor {
1063
1063
  debugSink?: (event: ShellCaptureDebugEvent) => void;
1064
1064
  /** Store the snapshot for navigation replay, never the captured HTML prelude. */
1065
1065
  navigationOnly?: true;
1066
+ /**
1067
+ * Set for a shell keyed by the visitor's URL (`rewrite(...).shell("visitor")`,
1068
+ * visitorShellKey): the capture renders the visitor's location
1069
+ * (`RequestContext._shellCaptureForVisitor`) and stores it as
1070
+ * ShellCacheEntry.pathname.
1071
+ */
1072
+ pathname?: string;
1066
1073
  /**
1067
1074
  * lastStoredCaptureSeq(key) when a document request read the store and
1068
1075
  * missed. The request schedules its capture only after rendering the MISS,
@@ -1878,6 +1885,9 @@ async function attemptCapture(
1878
1885
  descriptor.navigationOnly ? { request, url } : undefined,
1879
1886
  );
1880
1887
  const { derivedCtx, freshHandleStore } = derivation;
1888
+ if (descriptor.pathname !== undefined) {
1889
+ derivedCtx._shellCaptureForVisitor = true;
1890
+ }
1881
1891
  // The capture generation starts before matching or any snapshot read
1882
1892
  // (ShellCacheEntry.createdAt): a tag invalidated after it wins.
1883
1893
  const captureStartedAt = Date.now();
@@ -2647,6 +2657,7 @@ async function captureAndStoreShell(
2647
2657
  // default, #971). The serve tail replays it so the resume tree
2648
2658
  // matches the frozen prelude — see ShellCacheEntry.initialTheme.
2649
2659
  initialTheme: payloadInitialTheme(reqCtx),
2660
+ pathname: capture.pathname,
2650
2661
  snapshot,
2651
2662
  prunedRecords: snapshot.length > 0 ? prunedRecords : undefined,
2652
2663
  // 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). */