@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
@@ -109,10 +109,12 @@ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
109
109
  import { getContext } from "../../server/context.js";
110
110
  import { executionStart } from "../../cache/tag-invalidation.js";
111
111
  import {
112
+ bindRes,
112
113
  createRequestContext,
113
114
  runWithRequestContext,
114
115
  type RequestContext,
115
116
  } from "../../server/request-context.js";
117
+ import { recordUse } from "../segment-resolution/prefetch-deferral.js";
116
118
 
117
119
  /**
118
120
  * Re-render a cached route in the background and write it with
@@ -165,9 +167,7 @@ export async function rerenderAndCacheRoute<TEnv>(
165
167
  themeConfig: requestCtx._themeConfig,
166
168
  });
167
169
  const renderCtx: RequestContext<TEnv> = Object.assign(
168
- Object.create(requestCtx, {
169
- res: Object.getOwnPropertyDescriptor(sink, "res")!,
170
- }),
170
+ Object.create(requestCtx),
171
171
  {
172
172
  _handleStore: handleStore,
173
173
  _metricsStore: undefined,
@@ -184,6 +184,8 @@ export async function rerenderAndCacheRoute<TEnv>(
184
184
  _setKeepCacheDirective: sink._setKeepCacheDirective,
185
185
  },
186
186
  );
187
+ // ctx.res reads the sink's stub, not the live request's.
188
+ bindRes(renderCtx, () => sink.res);
187
189
  const store = Object.assign(Object.create(ctx.Store), { metrics: undefined });
188
190
  const runInStore = <T>(fn: () => T): T =>
189
191
  getContext().runWithStore(
@@ -281,7 +283,17 @@ export function withBackgroundRevalidation<TEnv>(
281
283
  // Only trigger background revalidation if:
282
284
  // 1. Cache was hit and stale
283
285
  // 2. Cache scope exists
284
- if (!state.shouldRevalidate || !state.cacheHit || !ctx.cacheScope) {
286
+ // 3. The request may refresh the record (RecordUse.refresh): a fill may
287
+ // not (prefetch: false). It runs no handler of a segment the client
288
+ // holds, and the re-render runs every handler of the route. The record
289
+ // stays stale until a request that reads it arrives: the prefetch
290
+ // before the fill already was one.
291
+ if (
292
+ !state.shouldRevalidate ||
293
+ !state.cacheHit ||
294
+ !ctx.cacheScope ||
295
+ !recordUse(ctx.handlerContext).refresh
296
+ ) {
285
297
  return;
286
298
  }
287
299
 
@@ -99,6 +99,10 @@ import { observeEvent } from "../instrument.js";
99
99
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
100
100
  import { treeHasStreaming } from "./segment-resolution.js";
101
101
  import { loaderPins } from "../segment-resolution/loader-cache.js";
102
+ import {
103
+ fillSends,
104
+ recordUse,
105
+ } from "../segment-resolution/prefetch-deferral.js";
102
106
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
103
107
  import {
104
108
  isStoredEntryStale,
@@ -345,14 +349,20 @@ async function* yieldFromStore<TEnv>(
345
349
  for (const segment of segments) {
346
350
  if (
347
351
  !ctx.isFullMatch &&
348
- !paramsChanged &&
349
352
  ctx.clientSegmentSet.has(segment.id) &&
350
- !(
351
- resendRouteSegments &&
352
- (segment.type === "route" || segment.belongsToRoute === true)
353
- )
353
+ // A stored intercept segment is re-sent, as on the runtime cache path:
354
+ // its loaders run (handleCacheHitIntercept), and a kept (null) one would
355
+ // be left out of the response with its loaders' new data.
356
+ !segment.namespace?.startsWith("intercept:")
354
357
  ) {
355
- keepClientSegment(segment);
358
+ // A fill keeps it (fillSends); otherwise it is sent again when the
359
+ // params changed or an action re-sends the route's own segments.
360
+ const sends =
361
+ fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ??
362
+ (paramsChanged ||
363
+ (resendRouteSegments &&
364
+ (segment.type === "route" || segment.belongsToRoute === true)));
365
+ if (!sends) keepClientSegment(segment);
356
366
  }
357
367
  yield segment;
358
368
  }
@@ -725,7 +735,15 @@ export function withCacheLookup<TEnv>(
725
735
  throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
726
736
  }
727
737
 
728
- if (ctx.isAction || !ctx.cacheScope?.enabled) {
738
+ // A prefetch that defers a layout above the boundary (prefetch: false)
739
+ // emits nothing the record holds: reading it would replay the handle
740
+ // pushes of segments the response does not carry. withCacheStore skips
741
+ // the write for the same request.
742
+ if (
743
+ ctx.isAction ||
744
+ !ctx.cacheScope?.enabled ||
745
+ !recordUse(ctx.handlerContext).read
746
+ ) {
729
747
  yield* source;
730
748
  if (ms) {
731
749
  ms.metrics.push({
@@ -919,6 +937,16 @@ export function withCacheLookup<TEnv>(
919
937
  continue;
920
938
  }
921
939
 
940
+ // A fill keeps a held segment outright (fillSends).
941
+ if (
942
+ fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ===
943
+ false
944
+ ) {
945
+ keepClientSegment(segment);
946
+ yield segment;
947
+ continue;
948
+ }
949
+
922
950
  const entryInfo = entryRevalidateMap?.get(segment.id);
923
951
 
924
952
  const searchChanged = ctx.prevUrl.search !== ctx.url.search;
@@ -114,6 +114,7 @@ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
114
114
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
115
115
  import type { GeneratorMiddleware } from "./cache-lookup.js";
116
116
  import { rerenderAndCacheRoute } from "./background-revalidation.js";
117
+ import { recordUse } from "../segment-resolution/prefetch-deferral.js";
117
118
 
118
119
  /**
119
120
  * Creates cache store middleware
@@ -149,11 +150,15 @@ export function withCacheStore<TEnv>(
149
150
  // segments were collected into allSegments either way).
150
151
  recordShellCaptureDocRecord(ctx, state, allSegments);
151
152
 
153
+ // prefetch: false (prefetch-deferral.ts). A prefetch that deferred a
154
+ // layout above the boundary did not read the record (withCacheLookup)
155
+ // and could only write an incomplete one.
152
156
  if (
153
157
  !ctx.cacheScope?.enabled ||
154
158
  ctx.isAction ||
155
159
  state.cacheHit ||
156
- ctx.request.method !== "GET"
160
+ ctx.request.method !== "GET" ||
161
+ !recordUse(ctx.handlerContext).write
157
162
  ) {
158
163
  if (ms) {
159
164
  ms.metrics.push({
@@ -192,6 +197,15 @@ export function withCacheStore<TEnv>(
192
197
  cacheScope.covers(s.id, s.namespace),
193
198
  );
194
199
 
200
+ // The client holds part of what the record covers, so writing it takes
201
+ // the proactive re-render below, which a request that may not refresh
202
+ // the record (RecordUse.refresh) does not start. That is a fill: it
203
+ // renders what the client is missing and nothing else, and writes like
204
+ // any other request only when that is everything the record covers. The
205
+ // stale-hit refresh skips it for the same reason
206
+ // (withBackgroundRevalidation).
207
+ if (hasNullComponents && !recordUse(ctx.handlerContext).refresh) return;
208
+
195
209
  const requestCtx = getRequestContext();
196
210
  if (!requestCtx) return;
197
211
 
@@ -260,7 +260,9 @@ async function handleCacheHitIntercept<TEnv>(
260
260
  interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
261
261
  debugLog(
262
262
  "matchPartial.intercept",
263
- "cache hit with fresh intercept loaders",
263
+ freshLoaderResult.loaderIds.length > 0
264
+ ? "cache hit with fresh intercept loaders"
265
+ : "cache hit, every intercept loader held",
264
266
  {
265
267
  routeName: ctx.localRouteName,
266
268
  slotName,
@@ -270,7 +272,7 @@ async function handleCacheHitIntercept<TEnv>(
270
272
  } else {
271
273
  debugLog(
272
274
  "matchPartial.intercept",
273
- "cache hit without intercept loader revalidation",
275
+ "cache hit, intercept has no loaders",
274
276
  {
275
277
  routeName: ctx.localRouteName,
276
278
  slotName,
@@ -108,6 +108,7 @@
108
108
  * 4. Render slot content if slots present
109
109
  */
110
110
  import type { MatchResult, ResolvedSegment } from "../types.js";
111
+ import { isPlaceholder } from "../types/segments.js";
111
112
  import type { MatchContext, MatchPipelineState } from "./match-context.js";
112
113
  import { debugLog } from "./logging.js";
113
114
  import { appendMetric } from "./metrics.js";
@@ -229,12 +230,23 @@ export function buildMatchResult<TEnv>(
229
230
  allIds = [...new Set(allIds)];
230
231
 
231
232
  // One pass over allSegments: keep renderable segments and collect the
232
- // handler-ran ids (resolvedIds) together.
233
+ // ids that ran (resolvedIds) together. A partial match resolves only the
234
+ // loaders it runs (revalidation.ts), so each loader segment here ran:
235
+ // the browser replaces that loader's handle pushes, and keeps those of a
236
+ // loader it skipped (handles/pushed-by.ts).
233
237
  const clientIdSet = new Set(ctx.clientSegmentIds);
234
238
  segmentsToRender = [];
235
239
  resolvedIds = [];
236
240
  for (const s of allSegments) {
237
- if (s._handlerRan) resolvedIds.push(s.id);
241
+ // A deferred loader (prefetch: false) did not run in this response:
242
+ // its pushes arrive with the fill, which lists it then.
243
+ if (s._handlerRan || (s.type === "loader" && !isPlaceholder(s))) {
244
+ resolvedIds.push(s.id);
245
+ }
246
+ // A deferred unit (prefetch: false) has a null component and is sent:
247
+ // only a segment the client does not hold is ever deferred, and
248
+ // deferral reads these same ids (prefetch-deferral.ts
249
+ // PrefetchDeferral.held), not the set the match forces renders with.
238
250
  if (
239
251
  s.component !== null ||
240
252
  s.type === "loader" ||
@@ -8,6 +8,7 @@ import type { GlobalReverseFunction } from "../reverse.js";
8
8
  import type { Theme } from "../theme/types.js";
9
9
  import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
10
10
  import type { RequestScope } from "../types/request-scope.js";
11
+ import type { Rewrite } from "../route-definition/rewrite.js";
11
12
 
12
13
  type GetVariableFn = {
13
14
  <T>(contextVar: ContextVar<T>): T | undefined;
@@ -81,6 +82,21 @@ export interface MiddlewareContext<
81
82
 
82
83
  /** Global route names only; see {@link GlobalReverseFunction}. */
83
84
  reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
85
+
86
+ /**
87
+ * Whether `value` is a `rewrite()` thrown downstream. A middleware that
88
+ * wraps `next()` in try/catch rethrows it, so the request re-dispatches:
89
+ *
90
+ * ```ts
91
+ * try {
92
+ * return await next();
93
+ * } catch (error) {
94
+ * if (ctx.isRewrite(error)) throw error;
95
+ * return new Response("Error", { status: 500 });
96
+ * }
97
+ * ```
98
+ */
99
+ isRewrite(value: unknown): value is Rewrite;
84
100
  }
85
101
 
86
102
  export type MiddlewareFn<
@@ -32,7 +32,14 @@ import {
32
32
  import { getCacheExecScope } from "../cache/cache-exec-scope.js";
33
33
  import { assertNonCacheableReadAllowed } from "../server/context.js";
34
34
  import { readGuardedTheme } from "../server/cookie-store.js";
35
+ import { defineSlot, slotAccessor } from "../server/slot-accessors.js";
35
36
  import type { Theme } from "../theme/types.js";
37
+ import {
38
+ isRewrite,
39
+ rewriteNotSupported,
40
+ type Rewrite,
41
+ } from "../route-definition/rewrite.js";
42
+ import { defineRewrittenFrom } from "../server/rewrite-trail.js";
36
43
 
37
44
  // Re-export types consumed through this module's path.
38
45
  export type {
@@ -43,19 +50,54 @@ export type {
43
50
  } from "./middleware-types.js";
44
51
 
45
52
  /**
46
- * Middleware ctx.theme: the visitor's theme cookie, guarded like the handler
47
- * ctx.theme and cookies() (#971, #946), so a read inside a "use cache"
48
- * function the middleware calls throws. Non-enumerable like the handler and
49
- * request-context getters: a spread of the ctx (inside a "use cache"
50
- * function, say) does not read it. One shared descriptor, so every ctx keeps
51
- * the same property shape.
53
+ * The middleware ctx's accessors: shared descriptors (slot-accessors.ts).
54
+ * routeName, _routeName, theme and setTheme read only the request context;
55
+ * headers calls the per-ctx closure in HEADERS_SLOT.
52
56
  */
53
- const MIDDLEWARE_THEME_DESCRIPTOR: PropertyDescriptor = {
54
- get(): Theme | undefined {
55
- return readGuardedTheme(_getRequestContext(), "ctx.theme");
57
+ const HEADERS_SLOT: unique symbol = Symbol("rango.middlewareContext.headers");
58
+
59
+ const MIDDLEWARE_ACCESSORS: Record<
60
+ "routeName" | "_routeName" | "headers" | "setTheme" | "theme",
61
+ PropertyDescriptor
62
+ > = {
63
+ // Re-derived from the request context on each access, so global middleware
64
+ // sees the matched route name after await next().
65
+ routeName: {
66
+ get(): string | undefined {
67
+ const raw = _getRequestContext()?._routeName;
68
+ return raw && !isAutoGeneratedRouteName(raw) ? raw : undefined;
69
+ },
70
+ enumerable: true,
71
+ configurable: true,
72
+ },
73
+ // The matched route key, auto-generated names included (the handler ctx's
74
+ // _routeName); undefined before routing.
75
+ _routeName: {
76
+ get(): string | undefined {
77
+ return _getRequestContext()?._routeName;
78
+ },
79
+ enumerable: true,
80
+ configurable: true,
81
+ },
82
+ headers: slotAccessor(HEADERS_SLOT, true),
83
+ setTheme: {
84
+ get(): MiddlewareContext<unknown>["setTheme"] {
85
+ return _getRequestContext()?.setTheme;
86
+ },
87
+ enumerable: true,
88
+ configurable: true,
89
+ },
90
+ // The visitor's theme cookie, guarded like the handler ctx.theme and
91
+ // cookies() (#971, #946), so a read inside a "use cache" function the
92
+ // middleware calls throws. Non-enumerable like the handler and
93
+ // request-context getters: a spread of the ctx does not read it.
94
+ theme: {
95
+ get(): Theme | undefined {
96
+ return readGuardedTheme(_getRequestContext(), "ctx.theme");
97
+ },
98
+ enumerable: false,
99
+ configurable: true,
56
100
  },
57
- enumerable: false,
58
- configurable: true,
59
101
  };
60
102
 
61
103
  const MIDDLEWARE_METRIC_DEPTH = 1;
@@ -252,15 +294,20 @@ export function createMiddlewareContext<TEnv>(
252
294
 
253
295
  // reqCtx captured once above: the request-scoped platform fields
254
296
  // (originalUrl, executionContext, waitUntil) are immutable per request,
255
- // so snapshotting beats re-reading ALS on every access. The lazy getters
256
- // below (routeName, theme, setTheme) stay lazy because those can change
257
- // during `await next()`.
297
+ // so snapshotting beats re-reading ALS on every access. The accessors
298
+ // (routeName, _routeName, theme, setTheme: MIDDLEWARE_ACCESSORS) read the
299
+ // request context on each access because those can change during
300
+ // `await next()`.
258
301
  //
259
302
  // Request-scoped like the handler ctx (issue #940): the NOCACHE_SYMBOL brand
260
303
  // below makes a "use cache" function taking it key by host, _routeName,
261
304
  // pathname, params and search; set/header/headers throw inside that
262
305
  // function, and non-cacheable get reads run the shared guard.
263
- const ctx: MiddlewareContext<TEnv> & { readonly _routeName?: string } = {
306
+ // Data members only: the accessors are MIDDLEWARE_ACCESSORS.
307
+ const fields: Omit<
308
+ MiddlewareContext<TEnv>,
309
+ "routeName" | "headers" | "setTheme"
310
+ > = {
264
311
  request,
265
312
  url,
266
313
  originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
@@ -269,39 +316,12 @@ export function createMiddlewareContext<TEnv>(
269
316
  env: env as MiddlewareContext<TEnv>["env"],
270
317
  params,
271
318
  build: reqCtx?.build ?? false,
319
+ isRewrite,
272
320
  dynamic(): void {
273
321
  reqCtx?.dynamic();
274
322
  },
275
323
  executionContext: reqCtx?.executionContext,
276
324
  waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
277
- // Getter: re-derives from request context on each access so that global
278
- // middleware sees the matched route name after await next().
279
- get routeName(): MiddlewareContext<TEnv>["routeName"] {
280
- const reqCtx = _getRequestContext();
281
- const raw = reqCtx?._routeName;
282
- return (
283
- raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
284
- ) as MiddlewareContext<TEnv>["routeName"];
285
- },
286
-
287
- // The matched route key, auto-generated names included (the handler
288
- // ctx's _routeName); undefined before routing.
289
- get _routeName(): string | undefined {
290
- return _getRequestContext()?._routeName;
291
- },
292
-
293
- // Guarded only inside a "use cache" body: the stamp covers a miss that
294
- // took this ctx as an argument, the exec scope a stale refresh (which does
295
- // not stamp its args, cache-runtime.ts). A write there is lost on a hit.
296
- get headers(): Headers {
297
- const headers = getResponse().headers;
298
- return (INSIDE_CACHE_EXEC as symbol) in ctx ||
299
- getCacheExecScope() !== undefined
300
- ? guardHeaderWrites(headers, () =>
301
- assertNotInsideCacheExec(ctx, "headers"),
302
- )
303
- : headers;
304
- },
305
325
 
306
326
  get: ((keyOrVar: any) => {
307
327
  // "use cache" only: intercept middleware runs inside a cache()
@@ -338,11 +358,6 @@ export function createMiddlewareContext<TEnv>(
338
358
  responseHolder.response.headers.set(name, value);
339
359
  },
340
360
 
341
- // theme: MIDDLEWARE_THEME_DESCRIPTOR, defined after this literal.
342
- get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
343
- return _getRequestContext()?.setTheme;
344
- },
345
-
346
361
  setLocationState(entries) {
347
362
  const reqCtx = _getRequestContext();
348
363
  if (!reqCtx) {
@@ -376,11 +391,46 @@ export function createMiddlewareContext<TEnv>(
376
391
  }
377
392
  },
378
393
  };
379
- Object.defineProperty(ctx, "theme", MIDDLEWARE_THEME_DESCRIPTOR);
394
+ const ctx = fields as MiddlewareContext<TEnv>;
395
+ // Guarded only inside a "use cache" body: the stamp covers a miss that
396
+ // took this ctx as an argument, the exec scope a stale refresh (which does
397
+ // not stamp its args, cache-runtime.ts). A write there is lost on a hit.
398
+ defineSlot(ctx, HEADERS_SLOT, (): Headers => {
399
+ const headers = getResponse().headers;
400
+ return (INSIDE_CACHE_EXEC as symbol) in ctx ||
401
+ getCacheExecScope() !== undefined
402
+ ? guardHeaderWrites(headers, () =>
403
+ assertNotInsideCacheExec(ctx, "headers"),
404
+ )
405
+ : headers;
406
+ });
407
+ Object.defineProperty(ctx, "routeName", MIDDLEWARE_ACCESSORS.routeName);
408
+ Object.defineProperty(ctx, "_routeName", MIDDLEWARE_ACCESSORS._routeName);
409
+ Object.defineProperty(ctx, "headers", MIDDLEWARE_ACCESSORS.headers);
410
+ Object.defineProperty(ctx, "setTheme", MIDDLEWARE_ACCESSORS.setTheme);
411
+ Object.defineProperty(ctx, "theme", MIDDLEWARE_ACCESSORS.theme);
412
+ defineRewrittenFrom(ctx, reqCtx, "middleware");
380
413
  (ctx as any)[NOCACHE_SYMBOL] = true;
381
414
  return ctx;
382
415
  }
383
416
 
417
+ /** A middleware threw a Rewrite after its next() started the downstream. */
418
+ function rewriteAfterNextError(handler: Function, rewrite: Rewrite): Error {
419
+ return new Error(
420
+ `Middleware "${handler.name || "(anonymous)"}" threw rewrite() after ` +
421
+ `calling next(). Throw rewrite() before await next().`,
422
+ { cause: rewrite },
423
+ );
424
+ }
425
+
426
+ /** A middleware returned a Rewrite: only a thrown one re-dispatches. */
427
+ function returnedRewriteError(handler: Function): Error {
428
+ return new Error(
429
+ `Middleware "${handler.name || "(anonymous)"}" returned a rewrite(). ` +
430
+ `Throw it instead: \`throw rewrite(...)\`, don't return it.`,
431
+ );
432
+ }
433
+
384
434
  /**
385
435
  * Match middleware entries against a pathname
386
436
  * Returns entries that match, with extracted params
@@ -584,6 +634,12 @@ export async function executeMiddleware<TEnv>(
584
634
  // downstream chain and overwrite responseHolder.response.
585
635
  let nextPromise: Promise<Response> | null = null;
586
636
  let nextResolvedAt: number | undefined;
637
+ // Whether next() rejected with a Rewrite: only then did nothing render
638
+ // (a downstream middleware rewrote before its own next()). Any other
639
+ // rejection can come after the route rendered (a route middleware that
640
+ // throws after its next(), the render's own error), so a rewrite thrown
641
+ // after it would re-dispatch onto the used handle store.
642
+ let nextRejectedWithRewrite = false;
587
643
  const wrappedNext = (): Promise<Response> => {
588
644
  if (nextPromise) {
589
645
  throw new Error(
@@ -599,6 +655,7 @@ export async function executeMiddleware<TEnv>(
599
655
  },
600
656
  (err) => {
601
657
  nextResolvedAt = performance.now();
658
+ nextRejectedWithRewrite = isRewrite(err);
602
659
  throw err;
603
660
  },
604
661
  );
@@ -612,6 +669,11 @@ export async function executeMiddleware<TEnv>(
612
669
  // middleware-over-middleware and the core handler underneath. Pass-through
613
670
  // when neither surface is active.
614
671
  let result: Response | void;
672
+ // A thrown rewrite() is control flow like a thrown Response: held past
673
+ // the span (which settles as success) and rethrown below, so it unwinds
674
+ // to the dispatcher (rsc/handler.ts) with no further code of this
675
+ // middleware run.
676
+ let rewrite: Rewrite | undefined;
615
677
  try {
616
678
  result = await observePhase(PHASES.middleware(metricLabel), async () => {
617
679
  try {
@@ -627,6 +689,10 @@ export async function executeMiddleware<TEnv>(
627
689
  // this convention (segment-resolution/helpers.ts keeps result handling
628
690
  // outside the span). Real errors propagate past the span.
629
691
  if (error instanceof Response) return error;
692
+ if (isRewrite(error)) {
693
+ rewrite = error;
694
+ return undefined;
695
+ }
630
696
  throw error;
631
697
  }
632
698
  });
@@ -635,6 +701,16 @@ export async function executeMiddleware<TEnv>(
635
701
  // error paths (idempotent guard in finishMiddleware).
636
702
  finishMiddleware();
637
703
  }
704
+ // A rewrite after this middleware's next() started the downstream chain
705
+ // (resolved, still running, or failed) would leave that render running
706
+ // against the context the next pass moves, or re-dispatch onto what it
707
+ // already pushed, and drop the response it built. Only a rewrite after
708
+ // next() rejected with a Rewrite (nothing rendered) unwinds.
709
+ if (rewrite && nextPromise && !nextRejectedWithRewrite) {
710
+ throw rewriteAfterNextError(entry.handler, rewrite);
711
+ }
712
+ if (rewrite) throw rewrite;
713
+ if (isRewrite(result)) throw returnedRewriteError(entry.handler);
638
714
 
639
715
  // Record post-next() processing time when middleware did work after
640
716
  // the downstream chain resolved (e.g. adding headers, logging).
@@ -805,10 +881,12 @@ export async function executeInterceptMiddleware<TEnv>(
805
881
  // runner settles rango.middleware as success, not STATUS_ERROR (same
806
882
  // reasoning as executeMiddleware's main chain). Real errors propagate.
807
883
  if (error instanceof Response) return error;
884
+ if (isRewrite(error)) throw rewriteNotSupported(error, "intercept");
808
885
  throw error;
809
886
  }
810
887
  },
811
888
  );
889
+ if (isRewrite(result)) throw returnedRewriteError(middleware);
812
890
 
813
891
  if (result instanceof Response) {
814
892
  earlyResponse = result;
@@ -1,3 +1,4 @@
1
+ import { isLoaderSegmentId } from "../browser/react/filter-segment-order.js";
1
2
  import { requestHeaders } from "../server/request-headers.js";
2
3
  import type { RouteMatchResult } from "./pattern-matching.js";
3
4
 
@@ -145,7 +146,7 @@ export async function resolveNavigation(
145
146
 
146
147
  const filteredSegmentIds = clientSegmentIds.filter((id) => {
147
148
  if (id.includes(".@")) return false;
148
- if (/D\d+\./.test(id)) return false;
149
+ if (isLoaderSegmentId(id)) return false;
149
150
  return true;
150
151
  });
151
152
 
@@ -20,7 +20,7 @@ import { makeIsAction } from "./is-action.js";
20
20
  /**
21
21
  * Options for revalidation evaluation
22
22
  */
23
- interface EvaluateRevalidationOptions<TEnv> {
23
+ export interface EvaluateRevalidationOptions<TEnv> {
24
24
  /** Current segment to evaluate */
25
25
  segment: ResolvedSegment;
26
26
  /** Previous route params (from route match, not segment) */
@@ -153,6 +153,31 @@ function sortSuffixParams(node: TrieNode): void {
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Give every static-children map in the trie a null prototype, in place, and
158
+ * return the trie (#1109). The walker indexes `node.s` with raw URL segments,
159
+ * so a map with Object.prototype would answer `/constructor` or `/__proto__`
160
+ * with an inherited member. The builder creates null-prototype maps; a trie
161
+ * that arrives as JSON (the build manifest) or across the dev runner boundary
162
+ * gets them here. Object.setPrototypeOf keeps a JSON-parsed map's fast
163
+ * properties, where Object.create(null) starts in dictionary mode.
164
+ */
165
+ export function nullPrototypeStaticChildren(node: TrieNode): TrieNode {
166
+ if (node.s) {
167
+ Object.setPrototypeOf(node.s, null);
168
+ for (const child of Object.values(node.s)) {
169
+ nullPrototypeStaticChildren(child);
170
+ }
171
+ }
172
+ if (node.xp) {
173
+ for (const child of Object.values(node.xp)) {
174
+ nullPrototypeStaticChildren(child.c);
175
+ }
176
+ }
177
+ if (node.p) nullPrototypeStaticChildren(node.p.c);
178
+ return node;
179
+ }
180
+
156
181
  /**
157
182
  * Insert a route into the trie. Optional params expand into two branches at
158
183
  * registration time (skip-first, then present), so each terminal lives at the
@@ -271,7 +296,9 @@ function insertSegments(
271
296
  const segment = segments[index];
272
297
 
273
298
  if (segment.type === "static") {
274
- if (!node.s) node.s = {};
299
+ // Null prototype: a segment named `constructor` or `__proto__` is an own
300
+ // key, never an inherited member to descend into and write a leaf on.
301
+ if (!node.s) node.s = Object.create(null) as Record<string, TrieNode>;
275
302
  if (!node.s[segment.value]) node.s[segment.value] = {};
276
303
  insertSegments(
277
304
  node.s[segment.value],
@@ -164,17 +164,19 @@ export async function resolveLoaders<TEnv>(
164
164
  // key off it.
165
165
  ...stampAwaitBeforeFlush(loaderEntry),
166
166
  loaderData: deps.wrapLoaderPromise(
167
- runInsideLoaderScope(() =>
168
- resolveLoaderData(
169
- loaderEntry,
170
- ctx,
171
- ctx.pathname,
172
- // The bake key rides for flagged loaders too: ssr:false
173
- // bakes at capture regardless of the entry's loading() lane
174
- // (loader-cache.ts capture branch) and its HIT-tail seed
175
- // overlay needs the same key.
176
- bakeLane || loaderEntry.bake === true ? segmentId : null,
177
- ),
167
+ runInsideLoaderScope(
168
+ () =>
169
+ resolveLoaderData(
170
+ loaderEntry,
171
+ ctx,
172
+ ctx.pathname,
173
+ // The bake key rides for flagged loaders too: ssr:false
174
+ // bakes at capture regardless of the entry's loading() lane
175
+ // (loader-cache.ts capture branch) and its HIT-tail seed
176
+ // overlay needs the same key.
177
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
178
+ ),
179
+ loader.$$id,
178
180
  ),
179
181
  entry,
180
182
  segmentId,
@@ -216,13 +218,15 @@ export async function resolveLoaders<TEnv>(
216
218
  const { loader } = loaderEntry;
217
219
  const segmentId = `${shortCode}D${i}.${loader.$$id}`;
218
220
  const wrapped = deps.wrapLoaderPromise(
219
- runInsideLoaderScope(() =>
220
- resolveLoaderData(
221
- loaderEntry,
222
- ctx,
223
- ctx.pathname,
224
- bakeLane || loaderEntry.bake === true ? segmentId : null,
225
- ),
221
+ runInsideLoaderScope(
222
+ () =>
223
+ resolveLoaderData(
224
+ loaderEntry,
225
+ ctx,
226
+ ctx.pathname,
227
+ bakeLane || loaderEntry.bake === true ? segmentId : null,
228
+ ),
229
+ loader.$$id,
226
230
  ),
227
231
  entry,
228
232
  segmentId,
@@ -118,6 +118,7 @@ import {
118
118
  isShellCaptureActive,
119
119
  createMaskedLoaderPromise,
120
120
  } from "./loader-mask.js";
121
+ import { loaderOwner } from "./prefetch-deferral.js";
121
122
  // Lazy-loaded to avoid pulling @vitejs/plugin-rsc/rsc into modules that
122
123
  // import segment-resolution but never use loader caching.
123
124
  let _serializeResult: typeof import("../../cache/segment-codec.js").serializeResult;
@@ -658,7 +659,7 @@ function executeLoaderData<TEnv>(
658
659
  // Handle pushes: the store and the owning segment are read synchronously
659
660
  // at kickoff, as createLoaderExecutor does for the body's own pushes.
660
661
  const handleStore = _getRequestContext()?._handleStore;
661
- const owningSegmentId = internal._currentSegmentId;
662
+ const owningSegmentId = loaderOwner(ctx, loaderId);
662
663
  // Record only pushes from THIS loader's body and the loaders it awaits via
663
664
  // ctx.use: the handler and sibling loaders push into the same store
664
665
  // concurrently. A dependency can still run on a HIT for another reader