@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
@@ -92,6 +92,12 @@ export type EntryPropCommon = {
92
92
  cache?: EntryCacheConfig;
93
93
  /** URL prefix from include() scope, used for MountContext on client */
94
94
  mountPath?: string;
95
+ /**
96
+ * Include scope the entry's handler and loaders resolve `ctx.reverse(".name")`
97
+ * in. Structural entries only (layout, cache, parallel, wrapping
98
+ * middleware); a route resolves from its own name. See ReverseScope.
99
+ */
100
+ reverseScope?: ReverseScope;
95
101
  /** clientUrls() group key (PathOptions.clientGroup); route entries only. */
96
102
  clientGroup?: string;
97
103
  };
@@ -124,6 +130,11 @@ export type EntryPropRender = {
124
130
  * on both DSL evaluations, whatever `ssr` did to `loading`.
125
131
  */
126
132
  loadingPrefetch?: false;
133
+ /**
134
+ * loading(fallback, { ssr: false }), on both DSL evaluations; a document
135
+ * then sets `loading` to false (applyDocumentModeLoading).
136
+ */
137
+ loadingSsr?: false;
127
138
  transition?: TransitionConfig;
128
139
  };
129
140
 
@@ -145,7 +156,8 @@ export type LoaderEntry = {
145
156
  * Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
146
157
  * per-isSSR (router/manifest.ts cache key), so the flag is already
147
158
  * request-mode-correct when resolveLoaders (fresh.ts) reads it and never
148
- * appears on navigation-lane entries.
159
+ * appears on navigation-lane entries. Set by documentModeLoader, which
160
+ * getDocumentModeEntry also applies to a lazy include's captured ancestors.
149
161
  */
150
162
  awaitBeforeFlush?: true;
151
163
  /**
@@ -163,6 +175,13 @@ export type LoaderEntry = {
163
175
  * (router/segment-resolution/prefetch-deferral.ts).
164
176
  */
165
177
  prefetch?: false;
178
+ /**
179
+ * The declaring layout's / parallel's / cache()'s include scope, for the
180
+ * loader body's `ctx.reverse(".name")` (its DSL kickoff carries it,
181
+ * runInsideLoaderScope). Unset on a route's loaders: they resolve from the
182
+ * route's name.
183
+ */
184
+ reverseScope?: ReverseScope;
166
185
  };
167
186
 
168
187
  /**
@@ -233,7 +252,6 @@ export type InterceptEntry = {
233
252
  slotName: `@${string}`; // e.g., "@modal"
234
253
  routeName: string; // e.g., "card"
235
254
  handler: ReactNode | Handler<any, any, any>;
236
- middleware: MiddlewareFn<any, any>[];
237
255
  loader: LoaderEntry[];
238
256
  loading?: ReactNode | false;
239
257
  transition?: TransitionConfig;
@@ -327,10 +345,22 @@ export type EntryData =
327
345
  EntryPropSegments &
328
346
  EntryPropRender);
329
347
 
348
+ /**
349
+ * Where an include()'s handler runs in the route tree. Hidden include-scope
350
+ * names (`$prefix_*`) derive from it, never from counter state, so build
351
+ * discovery and runtime expansion name every include the same (#1104).
352
+ */
353
+ export interface IncludeScopePosition {
354
+ /** Sibling indexes of the enclosing includes, "_"-joined ("" at a mount root). */
355
+ scopePath?: string;
356
+ /** The scopePath tail since the nearest include that added a name segment. */
357
+ hiddenScopePath?: string;
358
+ }
359
+
330
360
  /**
331
361
  * Tracked include info for build-time manifest generation
332
362
  */
333
- export interface TrackedInclude {
363
+ export interface TrackedInclude extends IncludeScopePosition {
334
364
  prefix: string;
335
365
  fullPrefix: string;
336
366
  namePrefix?: string;
@@ -352,7 +382,7 @@ export type CachedHeaderScope = {
352
382
  /**
353
383
  * Context stored in AsyncLocalStorage
354
384
  */
355
- interface HelperContext {
385
+ interface HelperContext extends IncludeScopePosition {
356
386
  manifest: Map<string, EntryData>;
357
387
  namespace: string;
358
388
  parent: EntryData | null;
@@ -364,7 +394,8 @@ interface HelperContext {
364
394
  * different routers don't clobber each other (route-map-builder.ts). */
365
395
  routerId?: string;
366
396
  metrics?: MetricsStore;
367
- /** True when rendering for SSR (document requests) */
397
+ /** True when rendering for SSR (document requests). DSL decisions on it
398
+ * go through the document-mode helpers (documentModeLoader). */
368
399
  isSSR?: boolean;
369
400
  /** URL patterns map for path() routes (route name -> pattern) */
370
401
  patterns?: Map<string, string>;
@@ -597,6 +628,8 @@ export const getContext = (): {
597
628
  trackedIncludes: store.trackedIncludes,
598
629
  cacheProfiles: store.cacheProfiles,
599
630
  includeScope: store.includeScope,
631
+ scopePath: store.scopePath,
632
+ hiddenScopePath: store.hiddenScopePath,
600
633
  // cachedHeaderScope (and insideCacheScope) deliberately NOT copied —
601
634
  // the header guard's middleware exemption depends on the latch dying
602
635
  // with the funnel scope (see assertCachedHeaderWriteAllowed).
@@ -641,6 +674,8 @@ export const getContext = (): {
641
674
  rootScoped: store?.rootScoped,
642
675
  trackedIncludes: store?.trackedIncludes,
643
676
  cacheProfiles: store?.cacheProfiles,
677
+ scopePath: store?.scopePath,
678
+ hiddenScopePath: store?.hiddenScopePath,
644
679
  },
645
680
  callback,
646
681
  );
@@ -763,6 +798,54 @@ export function getRootScoped(): boolean {
763
798
  return store?.rootScoped ?? true;
764
799
  }
765
800
 
801
+ /**
802
+ * The include scope a segment's dot-local `ctx.reverse(".name")` resolves in.
803
+ *
804
+ * One handler ctx serves every segment of a request and is built from the
805
+ * matched route's name (match-api.ts), so a parent module's layout resolved
806
+ * `.settings` from the matched CHILD module's prefix first: on
807
+ * `/acme/web/issues` the account layout's `.settings` hit the project
808
+ * module's route. Structural entries (layout, cache, parallel, wrapping
809
+ * middleware) and the loaders they declare capture the scope where the DSL
810
+ * defined them. Their handler gets a ctx whose `reverse` carries it
811
+ * (withReverseScope, router/handler-context.ts), and their loaders' DSL
812
+ * kickoff carries it in the loader scope (runInsideLoaderScope) for the
813
+ * loader ctx the executor builds. Global names and mount-param autofill do
814
+ * not change.
815
+ */
816
+ export interface ReverseScope {
817
+ /** Include name prefix ("account.project"); undefined outside named includes. */
818
+ readonly prefix: string | undefined;
819
+ /** getRootScoped() where the segment was defined: bare-name fallback allowed. */
820
+ readonly rootScoped: boolean;
821
+ }
822
+
823
+ /** The ReverseScope of the DSL position being evaluated. */
824
+ export function getReverseScope(): ReverseScope {
825
+ return { prefix: getNamePrefix(), rootScoped: getRootScoped() };
826
+ }
827
+
828
+ /**
829
+ * A createReverseFunction result (router/handler-context.ts). The internal
830
+ * 4th argument resolves dot names in a segment's ReverseScope instead of the
831
+ * route's name.
832
+ */
833
+ export type ScopableReverse = (
834
+ name: string,
835
+ hrefParams?: Record<string, string>,
836
+ search?: Record<string, unknown>,
837
+ segmentScope?: ReverseScope,
838
+ ) => string;
839
+
840
+ /** `reverse` with dot names resolved in `scope`, if any. */
841
+ export function scopeReverse(
842
+ reverse: ScopableReverse,
843
+ scope: ReverseScope | undefined,
844
+ ): ScopableReverse {
845
+ if (scope === undefined) return reverse;
846
+ return (name, hrefParams, search) => reverse(name, hrefParams, search, scope);
847
+ }
848
+
766
849
  /**
767
850
  * Stamp build-time scope identity onto a Static() definition at mount time.
768
851
  * The bake collector (prerender-collection.ts) renders defs OUTSIDE any
@@ -817,6 +900,61 @@ export function getIsolatedLazyParent(
817
900
  };
818
901
  }
819
902
 
903
+ /*
904
+ * The DSL's document-only decisions. A document (isSSR) evaluation applies
905
+ * them in dsl-helpers.ts; getDocumentModeEntry applies the same ones to a
906
+ * chain built without isSSR. A new ctx.isSSR decision gets a helper here,
907
+ * called from both.
908
+ */
909
+
910
+ /** loader(Def, { ssr: false }): a document awaits it before the first flush. */
911
+ export function documentModeLoader(entry: LoaderEntry): LoaderEntry {
912
+ return entry.bake ? { ...entry, awaitBeforeFlush: true } : entry;
913
+ }
914
+
915
+ /** loading(fallback, { ssr: false }): a document renders no fallback. */
916
+ export function applyDocumentModeLoading(entry: EntryPropRender): void {
917
+ if (entry.loadingSsr === false) entry.loading = false;
918
+ }
919
+
920
+ const documentModeEntries = new WeakMap<EntryData, EntryData>();
921
+
922
+ /**
923
+ * `entry`, its ancestors, orphan layouts and parallels as a document (isSSR)
924
+ * DSL evaluation builds them, from an evaluation without isSSR.
925
+ *
926
+ * include() captures its parent chain on router.ts routes() or
927
+ * lazy-includes.ts runExpansion, neither of which has isSSR, and loadManifest
928
+ * reuses it for every request. Handed to a document as is, every ancestor
929
+ * outside the include kept its navigation-mode decisions (#1179). Intercepts
930
+ * stay as they are: a document never resolves one.
931
+ *
932
+ * Memoized per entry, so an ancestor several includes share maps to one copy.
933
+ */
934
+ export function getDocumentModeEntry(entry: EntryData): EntryData {
935
+ const known = documentModeEntries.get(entry);
936
+ if (known) return known;
937
+ const copy = { ...entry };
938
+ documentModeEntries.set(entry, copy);
939
+ documentModeEntries.set(copy, copy);
940
+ copy.loader = entry.loader.map(documentModeLoader);
941
+ applyDocumentModeLoading(copy);
942
+ copy.layout = entry.layout.map(getDocumentModeEntry);
943
+ const parallel: ParallelEntries = {};
944
+ for (const slot of Object.keys(entry.parallel) as `@${string}`[]) {
945
+ const slotEntry = entry.parallel[slot];
946
+ if (slotEntry) {
947
+ parallel[slot] = getDocumentModeEntry(slotEntry) as ParallelEntryData;
948
+ }
949
+ }
950
+ copy.parallel = parallel;
951
+ if (entry.parent) copy.parent = getDocumentModeEntry(entry.parent);
952
+ if (entry.orphanOwner) {
953
+ copy.orphanOwner = getDocumentModeEntry(entry.orphanOwner);
954
+ }
955
+ return copy;
956
+ }
957
+
820
958
  export function getParallelEntries(
821
959
  parallels: ParallelEntries | EntryData[] | undefined,
822
960
  ): ParallelEntryData[] {
@@ -909,6 +1047,8 @@ interface LoaderScope {
909
1047
  active: true;
910
1048
  /** $$id of the DSL loader this scope resolves (runInsideLoaderScope). */
911
1049
  loaderId?: string;
1050
+ /** The declaring entry's ReverseScope (LoaderEntry.reverseScope). */
1051
+ reverseScope?: ReverseScope;
912
1052
  }
913
1053
  const loaderScopeALS: AsyncLocalStorage<LoaderScope> = ((globalThis as any)[
914
1054
  LOADER_SCOPE_KEY
@@ -1472,8 +1612,21 @@ export function assertCachedHeaderWriteAllowed(
1472
1612
  * are bypassed because loaders are always fresh (never cached) and
1473
1613
  * their side effects (setCookie, header, etc.) are safe.
1474
1614
  */
1475
- export function runInsideLoaderScope<T>(fn: () => T, loaderId?: string): T {
1476
- return loaderScopeALS.run({ active: true, loaderId }, fn);
1615
+ export function runInsideLoaderScope<T>(
1616
+ fn: () => T,
1617
+ loaderId?: string,
1618
+ reverseScope?: ReverseScope,
1619
+ ): T {
1620
+ return loaderScopeALS.run({ active: true, loaderId, reverseScope }, fn);
1621
+ }
1622
+
1623
+ /**
1624
+ * The ReverseScope of the DSL loader kickoff around this call: a loader the
1625
+ * kickoff starts, and one its body starts with ctx.use(), resolve dot names
1626
+ * in it. A handler's ctx.use(Loader) runs outside any kickoff: none.
1627
+ */
1628
+ export function getLoaderReverseScope(): ReverseScope | undefined {
1629
+ return loaderScopeALS.getStore()?.reverseScope;
1477
1630
  }
1478
1631
 
1479
1632
  /**
@@ -4,6 +4,7 @@ import {
4
4
  isInsideLoaderScope,
5
5
  } from "./context.js";
6
6
  import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
7
+ import { isThenable } from "../handles/is-thenable.js";
7
8
  import { pushedByKey } from "../handles/pushed-by.js";
8
9
  import { defineSlot } from "./slot-accessors.js";
9
10
 
@@ -47,8 +48,9 @@ function createLateHandlePushError(
47
48
  ): Error {
48
49
  const error = new Error(
49
50
  `Handle "${handleName}" for segment "${segmentId}" was pushed after handle collection completed. ` +
50
- `This usually means an async JSX subtree suspended and later tried to push a handle during streaming. ` +
51
- `Push handles from the route/layout handler or during the initial synchronous JSX render instead.`,
51
+ `This usually means an async component pushed it after an await. ` +
52
+ `Push from the route or layout handler, or reserve the slot there with ` +
53
+ `ctx.use(Handle).defer() and resolve it from the component.`,
52
54
  );
53
55
  error.name = "LateHandlePushError";
54
56
  return error;
@@ -253,12 +255,14 @@ export interface HandleStore {
253
255
  ): void;
254
256
 
255
257
  /**
256
- * push() a record's copy of `loaderId`'s settled push that stands
257
- * (restoreHandles): the record supplies the loader's value too. From then
258
- * on this request drops every settled, thenable-free push the loader makes
259
- * (live, or replayed from a cache entry, anywhere inside its body), and
260
- * keeps the ones holding a thenable (holdsThenable: what the record could
261
- * not keep), placed among the restored copies in the loader's push order.
258
+ * push() a record's copy of `loaderId`'s push that stands (restoreHandles):
259
+ * the record supplies the loader's value too. From then on this request
260
+ * drops every thenable-free push the loader makes (live, or replayed from
261
+ * a cache entry, anywhere inside its body), and keeps the ones holding a
262
+ * thenable (holdsThenable: what the record could not keep), placed among
263
+ * the restored copies in the loader's push order. A top-level promise is
264
+ * judged by its settled value, as the capture judged it (#1182): dropped
265
+ * when it settles thenable-free or rejects, else kept once it settles.
262
266
  */
263
267
  pushRestored(
264
268
  handleName: string,
@@ -277,6 +281,16 @@ export interface HandleStore {
277
281
  */
278
282
  setRecordAuthority(authority: (loaderId: string) => RecordAuthority): void;
279
283
 
284
+ /**
285
+ * Capture only (shell-capture.ts deriveShellCaptureContext): the slot
286
+ * holding `value` becomes a DSL-loader push without an owner, so the
287
+ * record and the prelude leave it out (getDataForSegment's
288
+ * `excludeLoaderPushes`). The capture owns a bake-lane loader's promise
289
+ * when it is pushed, and calls this once the promise settled to a value
290
+ * that holds one (#1182).
291
+ */
292
+ unrecordPush(handleName: string, segmentId: string, value: unknown): void;
293
+
280
294
  /**
281
295
  * `loaderId`'s run in this request settled (resolved or rejected), its
282
296
  * pushes made (loader-resolution.ts createLoaderExecutor). Its placeholders
@@ -665,6 +679,39 @@ export function createHandleStore(): HandleStore {
665
679
  return after;
666
680
  }
667
681
 
682
+ // A restored loader's top-level promise push, judged by its settled value
683
+ // as push(await value) would be: thenable-free, the record's copy stands
684
+ // (the capture recorded that value, #1182); holding a promise, it is kept
685
+ // where the loader's pushes go, once it settles; rejected, it is dropped.
686
+ // Tracked on the auxiliary lane, so the late channel stays open for it.
687
+ function judgeRestoredPromise(
688
+ store: HandleStore,
689
+ handleName: string,
690
+ segmentId: string,
691
+ value: unknown,
692
+ loaderId: string,
693
+ tag: SlotTag,
694
+ ): void {
695
+ const judged = Promise.resolve(value).then(
696
+ (settled) => {
697
+ if (!holdsThenable(settled)) {
698
+ const existing = data[handleName]?.[segmentId];
699
+ if (existing) matchRestoredSlot(existing, loaderId);
700
+ return;
701
+ }
702
+ const values = ((data[handleName] ??= {})[segmentId] ??= []);
703
+ insertSlot(values, restoredInsertIndex(values, loaderId), value, tag);
704
+ if (tag.replayOf !== undefined) {
705
+ addPending(pendingReplays, tag.replayOf, values);
706
+ }
707
+ version++;
708
+ signalEmission();
709
+ },
710
+ () => {},
711
+ );
712
+ store.trackAuxiliary(judged);
713
+ }
714
+
668
715
  function replayPush(
669
716
  store: HandleStore,
670
717
  replay: NonNullable<typeof replaying>,
@@ -859,10 +906,22 @@ export function createHandleStore(): HandleStore {
859
906
  restoredBy = restoringLoader(replayOf);
860
907
  }
861
908
  }
862
- if (restoredBy !== undefined && !holdsThenable(value)) {
863
- const existing = data[handleName]?.[segmentId];
864
- if (existing) matchRestoredSlot(existing, restoredBy);
865
- return;
909
+ if (restoredBy !== undefined) {
910
+ if (isThenable(value)) {
911
+ judgeRestoredPromise(this, handleName, segmentId, value, restoredBy, {
912
+ loader: (loaderPush ?? isInsideLoaderScope()) || undefined,
913
+ replayOf,
914
+ liveOf: replayOf === undefined ? restoredBy : undefined,
915
+ owner,
916
+ by: getHandlePushOwner() ?? replayOf ?? owner,
917
+ });
918
+ return;
919
+ }
920
+ if (!holdsThenable(value)) {
921
+ const existing = data[handleName]?.[segmentId];
922
+ if (existing) matchRestoredSlot(existing, restoredBy);
923
+ return;
924
+ }
866
925
  }
867
926
 
868
927
  if (!data[handleName]) {
@@ -976,6 +1035,15 @@ export function createHandleStore(): HandleStore {
976
1035
  authority = of;
977
1036
  },
978
1037
 
1038
+ unrecordPush(handleName: string, segmentId: string, value: unknown): void {
1039
+ const values = data[handleName]?.[segmentId];
1040
+ if (!values) return;
1041
+ const i = values.indexOf(value);
1042
+ if (i < 0) return;
1043
+ const tags = tagsFor(values);
1044
+ tags[i] = { ...tags[i], loader: true, owner: undefined };
1045
+ },
1046
+
979
1047
  settleLoaderRun(loaderId: string): void {
980
1048
  if (completed) return;
981
1049
  const hole = isHole(loaderId);
@@ -23,7 +23,7 @@ import {
23
23
  serializeStateCookie,
24
24
  } from "../browser/cookie-name.js";
25
25
  import type { LoaderDefinition, LoaderContext } from "../types.js";
26
- import type { ScopedReverseFunction } from "../reverse.js";
26
+ import type { AutofillAwareReverseFunction } from "../types/handler-context.js";
27
27
  import type {
28
28
  DefaultEnv,
29
29
  DefaultReverseRouteMap,
@@ -65,6 +65,8 @@ import {
65
65
  } from "../theme/constants.js";
66
66
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
67
67
  import type { RoutineTrace } from "../rsc/routine-plan.js";
68
+ import type { ActionContinuation } from "../rsc/server-action.js";
69
+ import type { FormActionOutcome } from "../rsc/progressive-enhancement.js";
68
70
  import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
69
71
  import {
70
72
  assertCachedHeaderWriteAllowed,
@@ -345,6 +347,13 @@ export interface RequestContext<
345
347
  */
346
348
  _shellCaptureRun?: boolean;
347
349
 
350
+ /**
351
+ * @internal Set beside `_shellCaptureRun` when the shell is keyed by the
352
+ * visitor's URL (`rewrite(...).shell("visitor")`): the capture renders the
353
+ * visitor's location and `ctx.rewrittenFrom`; the theme stays the default.
354
+ */
355
+ _shellCaptureForVisitor?: boolean;
356
+
348
357
  /**
349
358
  * @internal Bake-lane loader containers collected DURING a shell capture:
350
359
  * segment-key -> the loader's (pre-wrap) result promise. Populated by
@@ -359,9 +368,10 @@ export interface RequestContext<
359
368
 
360
369
  /**
361
370
  * @internal Set on a capture's derived context when a loader pushed a
362
- * value the record cannot keep (a deferred push, masked nested promises,
363
- * or a push outside a loader body): the loader records then carry
364
- * `runs: 1`, so a HIT still runs their bodies for those pushes.
371
+ * value the record cannot keep (masked nested promises, a pushed promise
372
+ * that settled to one, a live-lane loader's push, or a push outside a
373
+ * loader body): the loader records then carry `runs: 1`, so a HIT still
374
+ * runs their bodies for those pushes.
365
375
  */
366
376
  _shellCaptureUnrecordedLoaderPush?: true;
367
377
 
@@ -774,8 +784,15 @@ export interface RequestContext<
774
784
  * Generate URLs from route names.
775
785
  * Uses the global route map. After route matching, scoped (`.name`) resolution
776
786
  * works within the matched include() scope.
777
- */
778
- reverse: ScopedReverseFunction<
787
+ *
788
+ * Params are optional in the type because the fill depends on the phase.
789
+ * After the route match (route middleware, handlers, loaders, server actions)
790
+ * missing params come from the matched route's params; explicit params win.
791
+ * In global middleware before `next()` nothing is matched and the fill is
792
+ * empty, so a route with a required param throws `Missing param "<name>" for
793
+ * route "<route>"` at runtime (a param-less route still resolves).
794
+ */
795
+ reverse: AutofillAwareReverseFunction<
779
796
  Record<string, string>,
780
797
  DefaultReverseRouteMap
781
798
  >;
@@ -786,6 +803,13 @@ export interface RequestContext<
786
803
  /** @internal Previous route key (from the navigation source), used for revalidation */
787
804
  _prevRouteKey?: string;
788
805
 
806
+ /**
807
+ * @internal Leaving a rewritten page, the visitor's URL moved
808
+ * (router/match-api.ts): the default revalidation counts it as a search
809
+ * change, since the render may read `ctx.rewrittenFrom`.
810
+ */
811
+ _visitorMoved?: boolean;
812
+
789
813
  /**
790
814
  * @internal True while the post-action revalidation render is running (set by
791
815
  * revalidateAfterAction). The "use cache" runtime reads this to prefer
@@ -796,6 +820,19 @@ export interface RequestContext<
796
820
  */
797
821
  _inActionRevalidation?: boolean;
798
822
 
823
+ /**
824
+ * @internal The outcome of the action this request ran (a JS server
825
+ * action's continuation, a no-JS form post's form state), set before route
826
+ * middleware wraps the re-render. While it is unset, applyRewrite
827
+ * (rsc/rewrite-passes.ts) moves a mutation with its body, so the action
828
+ * runs on the target's pass, or refuses it (`disallowMutations()`, or a
829
+ * body middleware read); once set, the request moves to the target, whose
830
+ * pass re-renders from it instead of running the action again (#1145).
831
+ */
832
+ _actionRan?:
833
+ | { kind: "action"; continuation: ActionContinuation }
834
+ | { kind: "form"; outcome: FormActionOutcome };
835
+
799
836
  /**
800
837
  * @internal Render barrier for experimental `rendered()` API.
801
838
  * Resolves when all non-loader segments have settled and handle data
@@ -1017,6 +1054,7 @@ export type PublicRequestContext<
1017
1054
  | "_onDemandProducer"
1018
1055
  | "_searchParamsFilter"
1019
1056
  | "_shellCaptureRun"
1057
+ | "_shellCaptureForVisitor"
1020
1058
  | "_shellImplicitCache"
1021
1059
  | "_shellLoaderSeed"
1022
1060
  | "_shellCaptureLoaderRecords"
@@ -1048,7 +1086,9 @@ export type PublicRequestContext<
1048
1086
  | "_locationState"
1049
1087
  | "_routeName"
1050
1088
  | "_prevRouteKey"
1089
+ | "_visitorMoved"
1051
1090
  | "_inActionRevalidation"
1091
+ | "_actionRan"
1052
1092
  | "_reportedErrors"
1053
1093
  | "_renderErrors"
1054
1094
  | "_recoveredHandlerErrors"
@@ -45,6 +45,38 @@ export function requestHeaders(request: Request): Headers {
45
45
  return RAW_HEADERS.get(request) ?? request.headers;
46
46
  }
47
47
 
48
+ /**
49
+ * A copy of `request`'s headers for a bodyless mirror of it (the PE
50
+ * re-render, route middleware around it, a rewrite pass after an action),
51
+ * without the body-framing ones and `content-type`, so a body check gated on
52
+ * the content-type passes over a request that has no body.
53
+ */
54
+ export function bodylessHeaders(request: Request): Headers {
55
+ const headers = new Headers(requestHeaders(request));
56
+ headers.delete("content-length");
57
+ headers.delete("content-encoding");
58
+ headers.delete("transfer-encoding");
59
+ headers.delete("content-type");
60
+ return headers;
61
+ }
62
+
63
+ /**
64
+ * A bodyless mirror of `request` at `url`: `method`, its
65
+ * {@link bodylessHeaders}, the signal and workerd's `cf`.
66
+ */
67
+ export function bodylessRequest(
68
+ url: string | URL,
69
+ request: Request,
70
+ method: string,
71
+ ): Request {
72
+ return new Request(url, {
73
+ method,
74
+ headers: bodylessHeaders(request),
75
+ signal: request.signal,
76
+ cf: (request as { cf?: unknown }).cf,
77
+ } as RequestInit);
78
+ }
79
+
48
80
  /**
49
81
  * Shadow `request.headers` with an own, non-enumerable getter that calls
50
82
  * `beforeRead` and returns the request's own Headers (stable per request, so
@@ -11,8 +11,15 @@
11
11
  import { contextGet, contextSet, createVar } from "../context-var.js";
12
12
  import { RewriteLoopError } from "../errors.js";
13
13
  import { documentCacheStoresRender } from "../cache/document-cache-policy.js";
14
- import { guardIdentityRead, type IdentityReadWording } from "./context.js";
14
+ import {
15
+ guardIdentityRead,
16
+ recordLoaderIdentityRead,
17
+ refuseInCacheScope,
18
+ type IdentityReadWording,
19
+ } from "./context.js";
20
+ import { isInsideIdentityExempt } from "../cache/cache-exec-scope.js";
15
21
  import { stripInternalParams } from "../router/internal-params.js";
22
+ import type { RewriteShell } from "../route-definition/rewrite.js";
16
23
  import type { RequestContext } from "./request-context.js";
17
24
 
18
25
  /** Most rewrites one request may take; the next one is a RewriteLoopError. */
@@ -23,7 +30,8 @@ const REWRITE_TRAIL = createVar<readonly string[]>();
23
30
 
24
31
  type TrailContext = Pick<RequestContext<any>, "_variables">;
25
32
 
26
- function pathKey(url: URL): string {
33
+ /** A URL's path and search: what a rewrite hop, a target and a visitor's move compare. */
34
+ export function pathKey(url: URL): string {
27
35
  return url.pathname + url.search;
28
36
  }
29
37
 
@@ -60,6 +68,25 @@ export function recordRewriteHop(
60
68
  return hops;
61
69
  }
62
70
 
71
+ /** The `.shell()` choice of the hop that reached the route that renders. */
72
+ const REWRITE_SHELL = createVar<RewriteShell | undefined>();
73
+
74
+ export function recordRewriteShell(
75
+ reqCtx: TrailContext,
76
+ audience: RewriteShell | undefined,
77
+ ): void {
78
+ contextSet(reqCtx._variables, REWRITE_SHELL, audience);
79
+ }
80
+
81
+ /** The last hop's `rewrite(...).shell()` choice; undefined: none. */
82
+ export function rewriteShellAudience(
83
+ reqCtx: TrailContext | undefined,
84
+ ): RewriteShell | undefined {
85
+ return reqCtx?._variables
86
+ ? (contextGet(reqCtx._variables, REWRITE_SHELL) as RewriteShell | undefined)
87
+ : undefined;
88
+ }
89
+
63
90
  type AudienceContext = Pick<
64
91
  RequestContext<any>,
65
92
  "_shellCaptureRun" | "_documentCacheRender" | "res"
@@ -68,7 +95,8 @@ type AudienceContext = Pick<
68
95
  /**
69
96
  * A render other visitors may be served: a PPR shell capture, or a render
70
97
  * the document cache stores (its response opted in before next()). It shows
71
- * what a direct visit to its URL shows: no visitor theme, no visitor URL.
98
+ * no visitor theme, and no visitor URL unless it is a visitor-keyed shell
99
+ * capture (`_shellCaptureForVisitor`).
72
100
  */
73
101
  export function rendersForSharedAudience(reqCtx: AudienceContext): boolean {
74
102
  return reqCtx._shellCaptureRun === true || documentCacheStoresRender(reqCtx);
@@ -112,9 +140,9 @@ export type RewrittenFromReader = "render" | "middleware";
112
140
  * The read behind every `ctx.rewrittenFrom`. Guarded like cookies(): inside
113
141
  * a `cache()` boundary or a `"use cache"` function it throws, and a loader's
114
142
  * own `cache()` fill records it. A render other visitors may be served sees
115
- * `undefined`, as on a direct visit; any other render that sees the visitor's
116
- * URL marks `_payloadVisitorLocation`, so the document cache does not store
117
- * it.
143
+ * `undefined`, as on a direct visit, except a visitor-keyed shell capture,
144
+ * whose key covers the URL; any other render that sees the visitor's URL
145
+ * marks `_payloadVisitorLocation`, so the document cache does not store it.
118
146
  */
119
147
  function readRewrittenFrom(
120
148
  reqCtx: RequestContext<any> | undefined,
@@ -122,7 +150,15 @@ function readRewrittenFrom(
122
150
  ): string | undefined {
123
151
  if (!reqCtx) return undefined;
124
152
  if (reader === "render" && rendersForSharedAudience(reqCtx)) {
125
- return undefined;
153
+ if (!reqCtx._shellCaptureForVisitor) return undefined;
154
+ // guardIdentityRead minus the capture guard: a cache() boundary, a "use
155
+ // cache" body or a loader's own cache() inside the capture is keyed
156
+ // without the URL.
157
+ if (!isInsideIdentityExempt()) {
158
+ refuseInCacheScope("ctx.rewrittenFrom", REWRITTEN_FROM_READ, reqCtx);
159
+ recordLoaderIdentityRead("ctx.rewrittenFrom", REWRITTEN_FROM_READ.verb);
160
+ }
161
+ return rewriteTrail(reqCtx)?.[0];
126
162
  }
127
163
  guardIdentityRead(reqCtx, "ctx.rewrittenFrom", REWRITTEN_FROM_READ);
128
164
  const href = rewriteTrail(reqCtx)?.[0];
@@ -145,15 +181,25 @@ function copyFor(holder: object, href: string | undefined): URL | undefined {
145
181
  return copy;
146
182
  }
147
183
 
184
+ /**
185
+ * The ctx defineRewrittenFrom ran on: `holder`, or the one it derives from
186
+ * (a layout's ctx is an Object.create child of the route's, withReverseScope).
187
+ */
188
+ function definedOn(holder: object): object {
189
+ while (!SOURCES.has(holder)) holder = Object.getPrototypeOf(holder);
190
+ return holder;
191
+ }
192
+
148
193
  /**
149
194
  * Non-enumerable, like `ctx.theme`: serializing a ctx (React's dev debug
150
195
  * info) must not count as a read.
151
196
  */
152
197
  const SCOPED_DESCRIPTOR: PropertyDescriptor = {
153
198
  get(this: object): URL | undefined {
199
+ const owner = definedOn(this);
154
200
  const href = readRewrittenFrom(
155
- SOURCES.get(this),
156
- READERS.get(this) ?? "render",
201
+ SOURCES.get(owner),
202
+ READERS.get(owner) ?? "render",
157
203
  );
158
204
  return copyFor(this, href);
159
205
  },