@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  import { parseCookiesFromHeader } from "./cookie-parse.js";
15
+ import { defineHidden, defineSlot, slotAccessor } from "./slot-accessors.js";
15
16
  import type { CacheErrorCategory } from "../cache/cache-error.js";
16
17
  import type { CookieOptions } from "../router/middleware.js";
17
18
  import {
@@ -31,6 +32,7 @@ import type {
31
32
  import type { Handle } from "../handle.js";
32
33
  import {
33
34
  type ContextVar,
35
+ assignOwn,
34
36
  contextGet,
35
37
  contextSet,
36
38
  isNonCacheable,
@@ -78,6 +80,7 @@ import {
78
80
  readGuardedTheme,
79
81
  } from "./cookie-store.js";
80
82
  import { requestHeaders } from "./request-headers.js";
83
+ import { requestKind } from "../router/segment-resolution/prefetch-deferral.js";
81
84
  import {
82
85
  createReverseFunction,
83
86
  stripInternalParams,
@@ -93,6 +96,10 @@ import {
93
96
  type ExecutionStart,
94
97
  } from "../cache/invalidation-order.js";
95
98
  import { invariant } from "../errors.js";
99
+ import {
100
+ defineRequestRewrittenFrom,
101
+ defineRewrittenFrom,
102
+ } from "./rewrite-trail.js";
96
103
  import { isAutoGeneratedRouteName } from "../route-name.js";
97
104
 
98
105
  export interface RenderForegroundCursor {
@@ -713,6 +720,29 @@ export interface RequestContext<
713
720
  */
714
721
  _payloadVisitorTheme?: boolean;
715
722
 
723
+ /**
724
+ * @internal Set by the partial render when its payload carries deferred
725
+ * units (`prefetch: false`): what one page's prefetch left for the fill.
726
+ * The document cache does not store that response, whatever Cache-Control
727
+ * it ends up with (docs/design/prefetch-false.md, R7).
728
+ */
729
+ _payloadDeferred?: boolean;
730
+
731
+ /**
732
+ * @internal What the request says it is: a `<Link>` prefetch or a fill
733
+ * (`prefetch: false`), read once at creation
734
+ * (segment-resolution/prefetch-deferral.ts requestKind).
735
+ */
736
+ _requestKind?: "prefetch" | "fill";
737
+
738
+ /**
739
+ * @internal Set when a render shows the visitor's URL of a rewritten
740
+ * request: the payload or SSR seed (renderedLocation, rsc/full-payload.ts)
741
+ * or a `ctx.rewrittenFrom` read (server/rewrite-trail.ts). The document
742
+ * cache does not store that response: it is keyed by the target.
743
+ */
744
+ _payloadVisitorLocation?: boolean;
745
+
716
746
  /**
717
747
  * Attach location state entries to the current response.
718
748
  *
@@ -1012,6 +1042,9 @@ export type PublicRequestContext<
1012
1042
  | "_readTheme"
1013
1043
  | "_documentCacheRender"
1014
1044
  | "_payloadVisitorTheme"
1045
+ | "_payloadDeferred"
1046
+ | "_requestKind"
1047
+ | "_payloadVisitorLocation"
1015
1048
  | "_locationState"
1016
1049
  | "_routeName"
1017
1050
  | "_prevRouteKey"
@@ -1069,6 +1102,60 @@ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
1069
1102
  // AsyncLocalStorage instance for request context
1070
1103
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
1071
1104
 
1105
+ // The request context's accessors: shared descriptors (slot-accessors.ts).
1106
+ const RES_SLOT: unique symbol = Symbol("rango.requestContext.res");
1107
+ const RENDER_BARRIER_SLOT: unique symbol = Symbol(
1108
+ "rango.requestContext.renderBarrier",
1109
+ );
1110
+
1111
+ const RES_DESCRIPTOR: PropertyDescriptor = {
1112
+ ...slotAccessor(RES_SLOT, true),
1113
+ set(_: Response): void {
1114
+ throw new Error(
1115
+ "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
1116
+ );
1117
+ },
1118
+ };
1119
+
1120
+ // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1121
+ // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1122
+ // "use cache" entry stored for every visitor. Non-enumerable like the
1123
+ // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1124
+ // context (React dev debug info) must not count as a read. The guard reads
1125
+ // the ambient context, as cookies() does, and falls back to the receiver.
1126
+ const REQUEST_THEME_DESCRIPTOR: PropertyDescriptor = {
1127
+ get(this: RequestContext<any>): Theme | undefined {
1128
+ return readGuardedTheme(this, "getRequestContext().theme");
1129
+ },
1130
+ enumerable: false,
1131
+ configurable: true,
1132
+ };
1133
+
1134
+ // A fresh context enumerates _renderBarrier like its other fields (the
1135
+ // fetchable-loader ctx spreads it, loader-fetch.ts); a derived one never did.
1136
+ const RENDER_BARRIER_DESCRIPTOR: PropertyDescriptor = slotAccessor(
1137
+ RENDER_BARRIER_SLOT,
1138
+ true,
1139
+ );
1140
+ const DERIVED_RENDER_BARRIER_DESCRIPTOR: PropertyDescriptor = {
1141
+ ...RENDER_BARRIER_DESCRIPTOR,
1142
+ enumerable: false,
1143
+ };
1144
+
1145
+ /**
1146
+ * Define `ctx.res` as an own accessor that reads `get`. Assigning it throws
1147
+ * the read-only error. A context derived from another (Object.create) gets
1148
+ * its own response this way; copying the descriptor would read the
1149
+ * prototype's slot.
1150
+ */
1151
+ export function bindRes(
1152
+ ctx: RequestContext<any, any>,
1153
+ get: () => Response,
1154
+ ): void {
1155
+ defineSlot(ctx, RES_SLOT, get);
1156
+ Object.defineProperty(ctx, "res", RES_DESCRIPTOR);
1157
+ }
1158
+
1072
1159
  /**
1073
1160
  * Capture the current async context (RequestContext._runAtRequestEntry), taken
1074
1161
  * on handler entry before any router scope, so a nested dispatch (a
@@ -1398,10 +1485,10 @@ export function createRequestContext<TEnv>(
1398
1485
  }
1399
1486
  const result: Record<string, string> = {};
1400
1487
  for (const key of Object.keys(parsed)) {
1401
- if (!deleted.has(key)) result[key] = parsed[key];
1488
+ if (!deleted.has(key)) assignOwn(result, key, parsed[key]);
1402
1489
  }
1403
1490
  for (const [k, v] of mutations) {
1404
- if (v !== null) result[k] = v;
1491
+ if (v !== null) assignOwn(result, k, v);
1405
1492
  }
1406
1493
  return result;
1407
1494
  };
@@ -1445,12 +1532,18 @@ export function createRequestContext<TEnv>(
1445
1532
  };
1446
1533
 
1447
1534
  const cleanUrl = stripInternalParams(url);
1535
+ const originalUrl = new URL(request.url);
1448
1536
 
1449
- const ctx: RequestContext<TEnv> = {
1537
+ // Data members only: the accessors and hidden members are defined after it.
1538
+ const fields: Omit<
1539
+ RequestContext<TEnv>,
1540
+ "res" | "_renderBarrier" | "_readTheme" | "_readCookie" | "_readCookies"
1541
+ > = {
1450
1542
  env,
1451
1543
  request,
1452
1544
  url: cleanUrl,
1453
- originalUrl: new URL(request.url),
1545
+ originalUrl,
1546
+ _requestKind: requestKind(request, originalUrl),
1454
1547
  pathname: url.pathname,
1455
1548
  searchParams: cleanUrl.searchParams,
1456
1549
  _variables: variables,
@@ -1476,15 +1569,6 @@ export function createRequestContext<TEnv>(
1476
1569
  }) as RequestContext<TEnv>["set"],
1477
1570
  params: {} as Record<string, string>,
1478
1571
 
1479
- get res(): Response {
1480
- return stubResponse;
1481
- },
1482
- set res(_: Response) {
1483
- throw new Error(
1484
- "ctx.res is read-only. Use ctx.header() to set response headers, or cookies() for cookie mutations.",
1485
- );
1486
- },
1487
-
1488
1572
  // The visitor's cookies, guarded like cookies() (#976): before, a plain
1489
1573
  // read that a ppr shell, a cache() entry or a "use cache" entry stored
1490
1574
  // for every visitor.
@@ -1498,10 +1582,6 @@ export function createRequestContext<TEnv>(
1498
1582
  return effectiveCookies();
1499
1583
  },
1500
1584
 
1501
- // Non-enumerable, defined after this literal.
1502
- _readCookie: effectiveCookie,
1503
- _readCookies: effectiveCookies,
1504
-
1505
1585
  setCookie(name: string, value: string, options?: CookieOptions): void {
1506
1586
  assertNotInsideCacheExec(ctx, "setCookie");
1507
1587
  stubResponse.headers.append(
@@ -1690,8 +1770,6 @@ export function createRequestContext<TEnv>(
1690
1770
  this._onResponseCallbacks.push(callback);
1691
1771
  },
1692
1772
 
1693
- // theme: a guarded, non-enumerable getter defined after this literal.
1694
- _readTheme: getTheme,
1695
1773
  setTheme: themeConfig
1696
1774
  ? (theme: Theme) => {
1697
1775
  assertNotInsideCacheExec(ctx, "setTheme");
@@ -1717,14 +1795,15 @@ export function createRequestContext<TEnv>(
1717
1795
  _renderDiagnosticsEnabled: undefined,
1718
1796
  _handlerStart: undefined,
1719
1797
 
1720
- _renderBarrier: null as any,
1721
1798
  _resolveRenderBarrier: null as any,
1722
1799
  _renderBarrierSegmentOrder: undefined,
1723
1800
 
1724
1801
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
1725
1802
  };
1803
+ const ctx = fields as RequestContext<TEnv>;
1726
1804
 
1727
- wireRenderBarrier(ctx, handleStore);
1805
+ bindRes(ctx, () => stubResponse);
1806
+ wireRenderBarrier(ctx, handleStore, RENDER_BARRIER_DESCRIPTOR);
1728
1807
 
1729
1808
  ctx.use = createUseFunction({
1730
1809
  handleStore,
@@ -1732,41 +1811,70 @@ export function createRequestContext<TEnv>(
1732
1811
  getContext: () => ctx,
1733
1812
  });
1734
1813
 
1735
- // getRequestContext().theme is the visitor's cookie, guarded like cookies()
1736
- // (#971); before it was a plain read that a ppr shell, a cache() entry or a
1737
- // "use cache" entry stored for every visitor. Non-enumerable like the
1738
- // handler ctx.theme (handler-context.ts THEME_DESCRIPTOR): serializing the
1739
- // context (React dev debug info) must not count as a read. The guard reads
1740
- // the ambient context, as cookies() does, and falls back to the receiver.
1741
- // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1742
- // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1743
- // The same holds for the unguarded cookie reads (#976).
1744
- Object.defineProperties(ctx, {
1745
- theme: {
1746
- get(this: RequestContext<TEnv>): Theme | undefined {
1747
- return readGuardedTheme(this, "getRequestContext().theme");
1748
- },
1749
- enumerable: false,
1750
- configurable: true,
1751
- },
1752
- _readTheme: { enumerable: false },
1753
- _readCookie: { enumerable: false },
1754
- _readCookies: { enumerable: false },
1755
- });
1814
+ // theme: REQUEST_THEME_DESCRIPTOR. _readTheme is the unguarded read:
1815
+ // non-enumerable too, so a spread copy of the context (the fetchable-loader
1816
+ // ctx, loader-fetch.ts) cannot carry it. The same holds for the unguarded
1817
+ // cookie reads (#976). Added here, not flipped from enumerable: changing an
1818
+ // existing property's attributes costs V8 a map copy per request.
1819
+ Object.defineProperty(ctx, "theme", REQUEST_THEME_DESCRIPTOR);
1820
+ defineRequestRewrittenFrom(ctx);
1821
+ defineHidden(ctx, "_readTheme", getTheme);
1822
+ defineHidden(ctx, "_readCookie", effectiveCookie);
1823
+ defineHidden(ctx, "_readCookies", effectiveCookies);
1756
1824
 
1757
1825
  (ctx as any)[NOCACHE_SYMBOL] = true;
1758
1826
  // Non-enumerable: a self-reference would make the context circular for
1759
1827
  // JSON.stringify, spreads and React's dev debug-info serialization of props.
1760
- Object.defineProperty(ctx, "_requestRoot", {
1761
- value: ctx,
1762
- enumerable: false,
1763
- configurable: true,
1764
- writable: true,
1765
- });
1828
+ defineHidden(ctx, "_requestRoot", ctx);
1766
1829
  ctx._requestStart = executionStart();
1767
1830
  return ctx;
1768
1831
  }
1769
1832
 
1833
+ /**
1834
+ * Move a request context to a rewrite target for the next pass of the
1835
+ * request (rsc/rewrite-passes.ts, `throw rewrite()`). The context object
1836
+ * stays the same, so what the visitor's pass set is still there: variables
1837
+ * (the rewrite trail among them), the response stub (status, headers,
1838
+ * cookies), location state, theme, `dynamic()`, metrics, request tags,
1839
+ * onResponse callbacks, the handle store.
1840
+ *
1841
+ * Moved to the target: the request (`rawUrl`, the visitor's `_rsc*` params
1842
+ * included), url, originalUrl, pathname, searchParams and the request kind.
1843
+ * Cleared for the target's match: params, route name, reverse, the
1844
+ * classified route, the request mode, and the document cache's render mark
1845
+ * (its middleware runs again for the target). The loader memo behind
1846
+ * `getRequestContext().use()` is new: a loader that middleware started in
1847
+ * the visitor's pass read the visitor's URL. A pass never renders before it
1848
+ * rewrites: router/middleware.ts refuses a rewrite thrown after a next()
1849
+ * unless that next() rejected with a Rewrite (a downstream middleware
1850
+ * rewrote before its own next()), so there is no render state to reset.
1851
+ */
1852
+ export function rebaseRequestContext(
1853
+ ctx: RequestContext<any>,
1854
+ request: Request,
1855
+ rawUrl: URL,
1856
+ ): void {
1857
+ const cleanUrl = stripInternalParams(rawUrl);
1858
+ ctx.request = guardRequestHeaders(request);
1859
+ ctx.url = cleanUrl;
1860
+ ctx.originalUrl = rawUrl;
1861
+ ctx.pathname = cleanUrl.pathname;
1862
+ ctx.searchParams = cleanUrl.searchParams;
1863
+ ctx._requestKind = requestKind(request, rawUrl);
1864
+ ctx.params = {};
1865
+ ctx._routeName = undefined;
1866
+ ctx.routeName = undefined;
1867
+ ctx.reverse = createReverseFunction(getGlobalRouteMap(), undefined, {});
1868
+ ctx._classifiedRoute = undefined;
1869
+ ctx._requestMode = undefined;
1870
+ ctx._documentCacheRender = undefined;
1871
+ ctx.use = createUseFunction({
1872
+ handleStore: ctx._handleStore,
1873
+ loaderPromises: new Map(),
1874
+ getContext: () => ctx,
1875
+ });
1876
+ }
1877
+
1770
1878
  /**
1771
1879
  * Wire a fresh render barrier onto `ctx`, closure-bound to THIS ctx and THIS
1772
1880
  * handle store. Called by createRequestContext for every fresh context, and by
@@ -1783,10 +1891,14 @@ export function createRequestContext<TEnv>(
1783
1891
  * read the foreground's handle snapshot; the capture's fresh `_handleStore`
1784
1892
  * was invisible, so foreground per-request handle data could bake into the
1785
1893
  * shared shell.
1894
+ *
1895
+ * A derived context's `_renderBarrier` is non-enumerable (the default
1896
+ * descriptor); a fresh context's is enumerable like its other fields.
1786
1897
  */
1787
1898
  export function wireRenderBarrier(
1788
1899
  ctx: RequestContext<any, any>,
1789
1900
  handleStore: HandleStore,
1901
+ descriptor: PropertyDescriptor = DERIVED_RENDER_BARRIER_DESCRIPTOR,
1790
1902
  ): void {
1791
1903
  // Reset the whole barrier family as OWN properties. No-op for a fresh
1792
1904
  // context; for the derived capture context this shadows the foreground's
@@ -1833,25 +1945,23 @@ export function wireRenderBarrier(
1833
1945
  }
1834
1946
  if (resolveBarrier) resolveBarrier();
1835
1947
  };
1948
+ defineSlot(ctx, RENDER_BARRIER_SLOT, (): Promise<void> => {
1949
+ const p = barrierResolved
1950
+ ? Promise.resolve()
1951
+ : new Promise<void>((resolve) => {
1952
+ resolveBarrier = resolve;
1953
+ });
1954
+ Object.defineProperty(ctx, "_renderBarrier", {
1955
+ value: p,
1956
+ writable: false,
1957
+ configurable: false,
1958
+ });
1959
+ return p;
1960
+ });
1836
1961
  // defineProperty, not assignment: on a derived context the prototype's
1837
1962
  // _renderBarrier may already be a non-writable data property (the getter
1838
1963
  // pins it after first access), which would reject a plain assignment.
1839
- Object.defineProperty(ctx, "_renderBarrier", {
1840
- get() {
1841
- const p = barrierResolved
1842
- ? Promise.resolve()
1843
- : new Promise<void>((resolve) => {
1844
- resolveBarrier = resolve;
1845
- });
1846
- Object.defineProperty(ctx, "_renderBarrier", {
1847
- value: p,
1848
- writable: false,
1849
- configurable: false,
1850
- });
1851
- return p;
1852
- },
1853
- configurable: true,
1854
- });
1964
+ Object.defineProperty(ctx, "_renderBarrier", descriptor);
1855
1965
  }
1856
1966
 
1857
1967
  // Capture the Max-Age value so it can be parsed numerically. A leading zero
@@ -2083,6 +2193,7 @@ export function createUseFunction<TEnv>(
2083
2193
  );
2084
2194
  },
2085
2195
  };
2196
+ defineRewrittenFrom(loaderCtx, ctx);
2086
2197
  // "use cache" keys it by route, like the request ctx it reads (#940).
2087
2198
  (loaderCtx as any)[NOCACHE_SYMBOL] = true;
2088
2199
 
@@ -10,6 +10,8 @@
10
10
  * keeps the object a real Request, and those operations never call it.
11
11
  */
12
12
 
13
+ import { defineHidden } from "./slot-accessors.js";
14
+
13
15
  /** The Headers each shadowed request's getter returns. */
14
16
  const RAW_HEADERS = new WeakMap<Request, Headers>();
15
17
 
@@ -93,12 +95,12 @@ export function shadowRequestHeaders(
93
95
  configurable: true,
94
96
  });
95
97
  const clone = request.clone;
96
- defineMethod(request, "clone", function (this: Request): Request {
98
+ defineHidden(request, "clone", function (this: Request): Request {
97
99
  return shadowRequestHeaders(clone.call(this), beforeRead);
98
100
  });
99
101
  const inspect = (request as unknown as Record<symbol, unknown>)[INSPECT];
100
102
  if (typeof inspect === "function") {
101
- defineMethod(
103
+ defineHidden(
102
104
  request,
103
105
  INSPECT,
104
106
  function (this: Request, ...args: unknown[]) {
@@ -109,18 +111,6 @@ export function shadowRequestHeaders(
109
111
  return request;
110
112
  }
111
113
 
112
- function defineMethod(
113
- target: object,
114
- key: PropertyKey,
115
- value: (...args: never[]) => unknown,
116
- ): void {
117
- Object.defineProperty(target, key, {
118
- value,
119
- configurable: true,
120
- writable: true,
121
- });
122
- }
123
-
124
114
  /** The getter `key` resolves to on `target`'s prototype chain, if any. */
125
115
  function inheritedGetter(
126
116
  target: object,
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The URLs a rewritten request moved through (`throw rewrite()`), and what
3
+ * each context sees of them.
4
+ *
5
+ * One source: the trail lives in the request's variables under a token no
6
+ * user code can reach, so a middleware cannot reset the loop count. The
7
+ * visitor's URL is its first entry (`ctx.rewrittenFrom`); the hops are its
8
+ * length minus one.
9
+ */
10
+
11
+ import { contextGet, contextSet, createVar } from "../context-var.js";
12
+ import { RewriteLoopError } from "../errors.js";
13
+ import { documentCacheStoresRender } from "../cache/document-cache-policy.js";
14
+ import { guardIdentityRead, type IdentityReadWording } from "./context.js";
15
+ import { stripInternalParams } from "../router/internal-params.js";
16
+ import type { RequestContext } from "./request-context.js";
17
+
18
+ /** Most rewrites one request may take; the next one is a RewriteLoopError. */
19
+ export const MAX_REWRITE_HOPS = 5;
20
+
21
+ /** Clean hrefs, the visitor's first. */
22
+ const REWRITE_TRAIL = createVar<readonly string[]>();
23
+
24
+ type TrailContext = Pick<RequestContext<any>, "_variables">;
25
+
26
+ function pathKey(url: URL): string {
27
+ return url.pathname + url.search;
28
+ }
29
+
30
+ /** The trail of a rewritten request; undefined when nothing rewrote. */
31
+ export function rewriteTrail(
32
+ reqCtx: TrailContext | undefined,
33
+ ): readonly string[] | undefined {
34
+ return reqCtx?._variables
35
+ ? (contextGet(reqCtx._variables, REWRITE_TRAIL) as
36
+ | readonly string[]
37
+ | undefined)
38
+ : undefined;
39
+ }
40
+
41
+ /**
42
+ * Count one hop from `from` to `to` (clean URLs) and return its number.
43
+ * Throws RewriteLoopError past {@link MAX_REWRITE_HOPS}, or when `to` is the
44
+ * URL the request is on (pathname and search).
45
+ */
46
+ export function recordRewriteHop(
47
+ reqCtx: TrailContext,
48
+ from: URL,
49
+ to: URL,
50
+ ): number {
51
+ const trail = [...(rewriteTrail(reqCtx) ?? [from.href]), to.href];
52
+ const hops = trail.length - 1;
53
+ if (pathKey(to) === pathKey(from) || hops > MAX_REWRITE_HOPS) {
54
+ throw new RewriteLoopError(
55
+ trail.map((href) => pathKey(new URL(href))),
56
+ MAX_REWRITE_HOPS,
57
+ );
58
+ }
59
+ contextSet(reqCtx._variables, REWRITE_TRAIL, trail);
60
+ return hops;
61
+ }
62
+
63
+ type AudienceContext = Pick<
64
+ RequestContext<any>,
65
+ "_shellCaptureRun" | "_documentCacheRender" | "res"
66
+ >;
67
+
68
+ /**
69
+ * A render other visitors may be served: a PPR shell capture, or a render
70
+ * 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.
72
+ */
73
+ export function rendersForSharedAudience(reqCtx: AudienceContext): boolean {
74
+ return reqCtx._shellCaptureRun === true || documentCacheStoresRender(reqCtx);
75
+ }
76
+
77
+ /**
78
+ * The URL the visitor is on: the first URL of a rewritten request, else
79
+ * `url` with the internal `_rsc*` params stripped. A reload the router asks
80
+ * the browser for points here.
81
+ */
82
+ export function visitorUrl(reqCtx: TrailContext | undefined, url: URL): URL {
83
+ const first = rewriteTrail(reqCtx)?.[0];
84
+ return first ? new URL(first) : stripInternalParams(url);
85
+ }
86
+
87
+ const REWRITTEN_FROM_FIX =
88
+ "It is the visitor's URL, which the shared entry would serve to every " +
89
+ "visitor. Read it in middleware and pass what the page needs through a " +
90
+ "variable the cache key covers.";
91
+
92
+ const REWRITTEN_FROM_READ: IdentityReadWording = {
93
+ verb: "read",
94
+ fix: {
95
+ useCache:
96
+ "ctx.rewrittenFrom is not in the cache key. Read it before the cached " +
97
+ "function and pass it in as an argument.",
98
+ cacheScope: REWRITTEN_FROM_FIX,
99
+ capture: REWRITTEN_FROM_FIX,
100
+ warning: REWRITTEN_FROM_FIX,
101
+ },
102
+ };
103
+
104
+ /**
105
+ * Who reads `rewrittenFrom`: "render" for the contexts a render reads
106
+ * (handler, loader, response route, getRequestContext()), "middleware" for a
107
+ * middleware ctx, which runs once per request and is never stored.
108
+ */
109
+ export type RewrittenFromReader = "render" | "middleware";
110
+
111
+ /**
112
+ * The read behind every `ctx.rewrittenFrom`. Guarded like cookies(): inside
113
+ * a `cache()` boundary or a `"use cache"` function it throws, and a loader's
114
+ * 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.
118
+ */
119
+ function readRewrittenFrom(
120
+ reqCtx: RequestContext<any> | undefined,
121
+ reader: RewrittenFromReader,
122
+ ): string | undefined {
123
+ if (!reqCtx) return undefined;
124
+ if (reader === "render" && rendersForSharedAudience(reqCtx)) {
125
+ return undefined;
126
+ }
127
+ guardIdentityRead(reqCtx, "ctx.rewrittenFrom", REWRITTEN_FROM_READ);
128
+ const href = rewriteTrail(reqCtx)?.[0];
129
+ if (href && reader === "render") reqCtx._payloadVisitorLocation = true;
130
+ return href;
131
+ }
132
+
133
+ /** Each context's own copy of the URL, kept while the visitor URL holds. */
134
+ const COPIES = new WeakMap<object, URL>();
135
+ /** The request context a handler/loader/middleware ctx reads through. */
136
+ const SOURCES = new WeakMap<object, RequestContext<any> | undefined>();
137
+ const READERS = new WeakMap<object, RewrittenFromReader>();
138
+
139
+ function copyFor(holder: object, href: string | undefined): URL | undefined {
140
+ if (!href) return undefined;
141
+ const memo = COPIES.get(holder);
142
+ if (memo && memo.href === href) return memo;
143
+ const copy = new URL(href);
144
+ COPIES.set(holder, copy);
145
+ return copy;
146
+ }
147
+
148
+ /**
149
+ * Non-enumerable, like `ctx.theme`: serializing a ctx (React's dev debug
150
+ * info) must not count as a read.
151
+ */
152
+ const SCOPED_DESCRIPTOR: PropertyDescriptor = {
153
+ get(this: object): URL | undefined {
154
+ const href = readRewrittenFrom(
155
+ SOURCES.get(this),
156
+ READERS.get(this) ?? "render",
157
+ );
158
+ return copyFor(this, href);
159
+ },
160
+ enumerable: false,
161
+ configurable: true,
162
+ };
163
+
164
+ const REQUEST_DESCRIPTOR: PropertyDescriptor = {
165
+ get(this: RequestContext<any>): URL | undefined {
166
+ return copyFor(this, readRewrittenFrom(this, "render"));
167
+ },
168
+ enumerable: false,
169
+ configurable: true,
170
+ };
171
+
172
+ /**
173
+ * Define `ctx.rewrittenFrom` on a handler, loader, response-route or
174
+ * middleware ctx, read through `source` (the request context it belongs
175
+ * to). Each ctx gets its own copy of the URL.
176
+ */
177
+ export function defineRewrittenFrom(
178
+ ctx: object,
179
+ source: RequestContext<any> | undefined,
180
+ reader: RewrittenFromReader = "render",
181
+ ): void {
182
+ SOURCES.set(ctx, source);
183
+ if (reader !== "render") READERS.set(ctx, reader);
184
+ Object.defineProperty(ctx, "rewrittenFrom", SCOPED_DESCRIPTOR);
185
+ }
186
+
187
+ /** Define `rewrittenFrom` on a request context, read through itself. */
188
+ export function defineRequestRewrittenFrom(ctx: RequestContext<any>): void {
189
+ Object.defineProperty(ctx, "rewrittenFrom", REQUEST_DESCRIPTOR);
190
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Property definitions for per-request objects that keep V8 fast properties
3
+ * (#1109): the request context, the middleware ctx, the handle store and a
4
+ * guarded request.
5
+ *
6
+ * V8 puts an object in dictionary mode, where every property read is a hash
7
+ * lookup for the object's whole lifetime, when an object literal declares a
8
+ * get/set member, when a data property is redefined as an accessor after
9
+ * creation, or when an accessor is defined with a function its shape
10
+ * transition has not recorded: a closure created per request is a new
11
+ * function every time. So an accessor on a per-request object is a
12
+ * module-level descriptor, and one that needs per-object state reads a
13
+ * per-object closure from a non-enumerable symbol slot (slotAccessor +
14
+ * defineSlot). A derived object (Object.create(obj)) reads the slot through
15
+ * its prototype; the descriptor copied onto another object reads that
16
+ * object's slot instead. context-fast-properties.test.ts pins all of it.
17
+ *
18
+ * Dependency-free leaf: handle-store.ts is imported by request-context.ts,
19
+ * so these cannot live in either.
20
+ */
21
+
22
+ type SlotHolder = Record<symbol, () => unknown>;
23
+
24
+ /**
25
+ * An accessor descriptor whose getter calls the receiver's `slot` closure.
26
+ * Call it once at module level and share the result.
27
+ */
28
+ export function slotAccessor(
29
+ slot: symbol,
30
+ enumerable: boolean,
31
+ ): PropertyDescriptor {
32
+ return {
33
+ get(this: SlotHolder): unknown {
34
+ return this[slot]();
35
+ },
36
+ enumerable,
37
+ configurable: true,
38
+ };
39
+ }
40
+
41
+ /** Define `slot` on `target` as a non-enumerable, read-only data property. */
42
+ export function defineSlot(target: object, slot: symbol, value: unknown): void {
43
+ Object.defineProperty(target, slot, { value, configurable: true });
44
+ }
45
+
46
+ /** Define `key` on `target` as a non-enumerable, writable data property. */
47
+ export function defineHidden(
48
+ target: object,
49
+ key: PropertyKey,
50
+ value: unknown,
51
+ ): void {
52
+ Object.defineProperty(target, key, {
53
+ value,
54
+ writable: true,
55
+ configurable: true,
56
+ });
57
+ }
package/src/server.ts CHANGED
@@ -58,3 +58,7 @@ export {
58
58
  setClientUrlProjection,
59
59
  type ClientUrlProjection,
60
60
  } from "./client-urls/server-projection.js";
61
+
62
+ // Hoisted clientUrls() revalidate() predicates: the generated
63
+ // `<file>?rango-revalidate` modules register them in the RSC realm.
64
+ export { registerClientUrlRevalidators } from "./client-urls/revalidate-registry.js";