@rangojs/router 0.15.1 → 0.17.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 (249) hide show
  1. package/README.md +254 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  4. package/dist/types/browser/partial-update.d.ts +11 -0
  5. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  6. package/dist/types/browser/rsc-router.d.ts +1 -1
  7. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  8. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  9. package/dist/types/cache/cache-scope.d.ts +34 -5
  10. package/dist/types/cache/handle-capture.d.ts +41 -3
  11. package/dist/types/cache/handle-snapshot.d.ts +53 -13
  12. package/dist/types/cache/segment-codec.d.ts +10 -3
  13. package/dist/types/cache/taint.d.ts +1 -1
  14. package/dist/types/cache/types.d.ts +15 -3
  15. package/dist/types/client.d.ts +4 -4
  16. package/dist/types/client.rsc.d.ts +4 -5
  17. package/dist/types/components/DefaultDocument.d.ts +5 -2
  18. package/dist/types/context-var.d.ts +6 -5
  19. package/dist/types/escape-script.d.ts +1 -1
  20. package/dist/types/handles/MetaTags.d.ts +5 -4
  21. package/dist/types/handles/Scripts.d.ts +8 -7
  22. package/dist/types/handles/meta.d.ts +2 -2
  23. package/dist/types/handles/script.d.ts +17 -16
  24. package/dist/types/href-client.d.ts +1 -1
  25. package/dist/types/html.d.ts +12 -0
  26. package/dist/types/index.d.ts +1 -1
  27. package/dist/types/index.rsc.d.ts +3 -2
  28. package/dist/types/loader-store.d.ts +52 -0
  29. package/dist/types/reverse.d.ts +36 -4
  30. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  31. package/dist/types/route-definition/helpers-types.d.ts +36 -10
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +3 -1
  34. package/dist/types/router/debug-manifest.d.ts +3 -2
  35. package/dist/types/router/error-handling.d.ts +20 -0
  36. package/dist/types/router/handler-context.d.ts +6 -0
  37. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  38. package/dist/types/router/middleware-types.d.ts +3 -2
  39. package/dist/types/router/router-context.d.ts +3 -1
  40. package/dist/types/router/router-interfaces.d.ts +24 -2
  41. package/dist/types/router/router-options.d.ts +8 -5
  42. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  43. package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
  44. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  45. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
  46. package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
  47. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  48. package/dist/types/router/segment-wrappers.d.ts +1 -1
  49. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  50. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  51. package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
  52. package/dist/types/rsc/shell-capture.d.ts +5 -4
  53. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  54. package/dist/types/rsc/types.d.ts +10 -0
  55. package/dist/types/server/context.d.ts +33 -3
  56. package/dist/types/server/handle-store.d.ts +26 -2
  57. package/dist/types/server/request-context.d.ts +24 -21
  58. package/dist/types/ssr/index.d.ts +15 -0
  59. package/dist/types/static-handler.d.ts +4 -1
  60. package/dist/types/testing/generated-routes.d.ts +2 -2
  61. package/dist/types/testing/internal/context.d.ts +1 -1
  62. package/dist/types/testing/render-route.d.ts +32 -6
  63. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  64. package/dist/types/testing/vitest.d.ts +35 -1
  65. package/dist/types/theme/ThemeScript.d.ts +3 -3
  66. package/dist/types/thenable-status.d.ts +9 -0
  67. package/dist/types/types/cache-types.d.ts +14 -10
  68. package/dist/types/types/handler-context.d.ts +14 -0
  69. package/dist/types/types/loader-types.d.ts +8 -6
  70. package/dist/types/urls/response-types.d.ts +3 -12
  71. package/dist/types/use-loader.d.ts +12 -3
  72. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  73. package/dist/types/vite/rango.d.ts +1 -1
  74. package/dist/vite/index.js +884 -768
  75. package/package.json +4 -4
  76. package/skills/api-client/SKILL.md +22 -10
  77. package/skills/breadcrumbs/SKILL.md +65 -22
  78. package/skills/bundle-analysis/SKILL.md +22 -22
  79. package/skills/cache-guide/SKILL.md +207 -97
  80. package/skills/caching/SKILL.md +295 -93
  81. package/skills/catalog.json +16 -16
  82. package/skills/client-urls/SKILL.md +53 -31
  83. package/skills/cloudflare/SKILL.md +72 -20
  84. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  85. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  86. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  87. package/skills/comparison/SKILL.md +5 -0
  88. package/skills/comparison/references/framework-comparison.md +39 -33
  89. package/skills/composability/SKILL.md +38 -29
  90. package/skills/css/SKILL.md +10 -13
  91. package/skills/debug-manifest/SKILL.md +74 -31
  92. package/skills/defer-hydration/SKILL.md +2 -1
  93. package/skills/deployment-caching/SKILL.md +6 -1
  94. package/skills/document-cache/SKILL.md +92 -28
  95. package/skills/fonts/SKILL.md +16 -8
  96. package/skills/handler-use/SKILL.md +53 -17
  97. package/skills/hooks/SKILL.md +28 -20
  98. package/skills/hooks/data.md +43 -1
  99. package/skills/hooks/handle-and-actions.md +31 -16
  100. package/skills/hooks/navigation.md +39 -10
  101. package/skills/hooks/outlets.md +13 -6
  102. package/skills/hooks/state.md +37 -15
  103. package/skills/hooks/urls.md +7 -5
  104. package/skills/host-router/SKILL.md +64 -12
  105. package/skills/i18n/SKILL.md +60 -34
  106. package/skills/intercept/SKILL.md +161 -26
  107. package/skills/layout/SKILL.md +106 -50
  108. package/skills/links/SKILL.md +51 -19
  109. package/skills/loader/SKILL.md +242 -106
  110. package/skills/middleware/SKILL.md +119 -23
  111. package/skills/migrate-nextjs/SKILL.md +100 -45
  112. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  113. package/skills/migrate-react-router/SKILL.md +49 -38
  114. package/skills/migrate-react-router/component-migration.md +25 -6
  115. package/skills/migrate-react-router/data-and-actions.md +18 -5
  116. package/skills/migrate-react-router/route-mapping.md +12 -6
  117. package/skills/mime-routes/SKILL.md +12 -7
  118. package/skills/observability/SKILL.md +28 -14
  119. package/skills/parallel/SKILL.md +75 -39
  120. package/skills/ppr/SKILL.md +345 -150
  121. package/skills/prerender/SKILL.md +189 -100
  122. package/skills/rango/SKILL.md +65 -34
  123. package/skills/react-compiler/SKILL.md +6 -5
  124. package/skills/response-routes/SKILL.md +133 -25
  125. package/skills/route/SKILL.md +165 -60
  126. package/skills/router-setup/SKILL.md +418 -240
  127. package/skills/scripts/SKILL.md +31 -20
  128. package/skills/server-actions/SKILL.md +220 -59
  129. package/skills/shell-manifest/SKILL.md +12 -7
  130. package/skills/streams-and-websockets/SKILL.md +32 -14
  131. package/skills/tailwind/SKILL.md +37 -33
  132. package/skills/testing/SKILL.md +68 -32
  133. package/skills/testing/cache-prerender.md +55 -29
  134. package/skills/testing/client-components.md +59 -6
  135. package/skills/testing/e2e-parity.md +12 -2
  136. package/skills/testing/flight.md +19 -11
  137. package/skills/testing/handles.md +8 -5
  138. package/skills/testing/loader.md +8 -5
  139. package/skills/testing/middleware.md +8 -3
  140. package/skills/testing/predicates.md +120 -0
  141. package/skills/testing/render-handler.md +70 -22
  142. package/skills/testing/response-routes.md +11 -4
  143. package/skills/testing/reverse-and-types.md +20 -14
  144. package/skills/testing/server-actions.md +12 -3
  145. package/skills/testing/server-tree.md +9 -0
  146. package/skills/testing/setup.md +9 -5
  147. package/skills/theme/SKILL.md +106 -29
  148. package/skills/typesafety/SKILL.md +20 -9
  149. package/skills/typesafety/env-and-bindings.md +126 -47
  150. package/skills/typesafety/generated-files-and-cli.md +26 -14
  151. package/skills/typesafety/params-and-search.md +44 -19
  152. package/skills/typesafety/route-types.md +50 -14
  153. package/skills/use-cache/SKILL.md +200 -82
  154. package/skills/vercel/SKILL.md +21 -11
  155. package/skills/view-transitions/SKILL.md +41 -20
  156. package/src/__augment-tests__/augmented.check.ts +26 -1
  157. package/src/browser/index.ts +2 -2
  158. package/src/browser/navigation-bridge.ts +3 -4
  159. package/src/browser/navigation-transaction.ts +22 -2
  160. package/src/browser/partial-update.ts +75 -80
  161. package/src/browser/react/ScrollRestoration.tsx +6 -6
  162. package/src/browser/rsc-router.tsx +1 -1
  163. package/src/browser/scroll-restoration.ts +53 -6
  164. package/src/cache/cache-exec-scope.ts +37 -4
  165. package/src/cache/cache-key-utils.ts +1 -6
  166. package/src/cache/cache-runtime.ts +230 -61
  167. package/src/cache/cache-scope.ts +76 -12
  168. package/src/cache/document-cache.ts +15 -0
  169. package/src/cache/handle-capture.ts +90 -10
  170. package/src/cache/handle-snapshot.ts +132 -28
  171. package/src/cache/segment-codec.ts +26 -7
  172. package/src/cache/tag-invalidation.ts +4 -3
  173. package/src/cache/taint.ts +1 -1
  174. package/src/cache/types.ts +16 -3
  175. package/src/client.rsc.tsx +10 -9
  176. package/src/client.tsx +5 -4
  177. package/src/components/DefaultDocument.tsx +7 -2
  178. package/src/context-var.ts +6 -5
  179. package/src/escape-script.ts +1 -1
  180. package/src/handles/MetaTags.tsx +13 -11
  181. package/src/handles/Scripts.tsx +11 -10
  182. package/src/handles/meta.ts +2 -2
  183. package/src/handles/script.ts +17 -16
  184. package/src/href-client.ts +1 -1
  185. package/src/html.ts +12 -0
  186. package/src/index.rsc.ts +5 -0
  187. package/src/index.ts +1 -0
  188. package/src/loader-store.ts +106 -0
  189. package/src/prerender/build-shell-capture.ts +8 -11
  190. package/src/reverse.ts +44 -4
  191. package/src/route-definition/dsl-helpers.ts +134 -42
  192. package/src/route-definition/helpers-types.ts +36 -10
  193. package/src/route-definition/resolve-handler-use.ts +121 -6
  194. package/src/route-types.ts +2 -3
  195. package/src/router/debug-manifest.ts +10 -2
  196. package/src/router/error-handling.ts +51 -15
  197. package/src/router/handler-context.ts +34 -25
  198. package/src/router/intercept-resolution.ts +125 -39
  199. package/src/router/loader-resolution.ts +56 -11
  200. package/src/router/match-api.ts +12 -17
  201. package/src/router/match-middleware/background-revalidation.ts +161 -96
  202. package/src/router/match-middleware/cache-lookup.ts +93 -10
  203. package/src/router/match-middleware/cache-store.ts +17 -90
  204. package/src/router/middleware-types.ts +3 -5
  205. package/src/router/middleware.ts +52 -9
  206. package/src/router/prerender-match.ts +53 -19
  207. package/src/router/route-snapshot.ts +11 -1
  208. package/src/router/router-context.ts +2 -0
  209. package/src/router/router-interfaces.ts +25 -2
  210. package/src/router/router-options.ts +8 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -12
  212. package/src/router/segment-resolution/loader-cache.ts +205 -45
  213. package/src/router/segment-resolution/loader-mask.ts +12 -22
  214. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  215. package/src/router/segment-resolution/mask-nested.ts +7 -7
  216. package/src/router/segment-resolution/revalidation.ts +34 -6
  217. package/src/router/segment-wrappers.ts +4 -0
  218. package/src/rsc/render-pipeline.ts +3 -0
  219. package/src/rsc/response-route-handler.ts +11 -3
  220. package/src/rsc/rsc-rendering.ts +8 -45
  221. package/src/rsc/shell-capture-constants.ts +13 -0
  222. package/src/rsc/shell-capture.ts +111 -90
  223. package/src/rsc/ssr-setup.ts +14 -8
  224. package/src/rsc/types.ts +11 -0
  225. package/src/server/context.ts +88 -11
  226. package/src/server/cookie-store.ts +5 -4
  227. package/src/server/handle-store.ts +232 -7
  228. package/src/server/request-context.ts +35 -30
  229. package/src/ssr/index.tsx +40 -7
  230. package/src/ssr-suspension-warning.ts +2 -13
  231. package/src/static-handler.ts +4 -1
  232. package/src/testing/flight-runtime.d.ts +21 -0
  233. package/src/testing/generated-routes.ts +2 -2
  234. package/src/testing/internal/context.ts +1 -1
  235. package/src/testing/internal/flight-client-globals.ts +7 -5
  236. package/src/testing/render-route.tsx +85 -24
  237. package/src/testing/run-loader.ts +5 -1
  238. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  239. package/src/testing/vitest.ts +62 -1
  240. package/src/theme/ThemeProvider.tsx +14 -12
  241. package/src/theme/ThemeScript.tsx +3 -3
  242. package/src/thenable-status.ts +16 -0
  243. package/src/types/cache-types.ts +14 -10
  244. package/src/types/handler-context.ts +14 -0
  245. package/src/types/loader-types.ts +8 -6
  246. package/src/urls/response-types.ts +3 -23
  247. package/src/use-loader.tsx +24 -4
  248. package/src/vite/plugins/use-cache-transform.ts +118 -74
  249. package/src/vite/rango.ts +7 -3
@@ -103,7 +103,7 @@
103
103
  import type { ResolvedSegment } from "../../types.js";
104
104
  import {
105
105
  getRequestContext,
106
- runWithRequestContext,
106
+ type RequestContext,
107
107
  } from "../../server/request-context.js";
108
108
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
109
109
  import { createShellImplicitDocScope } from "../../cache/cache-scope.js";
@@ -111,6 +111,7 @@ import { getRouterContext } from "../router-context.js";
111
111
  import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
112
112
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
113
113
  import type { GeneratorMiddleware } from "./cache-lookup.js";
114
+ import { rerenderAndCacheRoute } from "./background-revalidation.js";
114
115
 
115
116
  /**
116
117
  * Creates cache store middleware
@@ -159,13 +160,9 @@ export function withCacheStore<TEnv>(
159
160
  return;
160
161
  }
161
162
 
162
- const {
163
- createHandlerContext,
164
- setupLoaderAccess,
165
- resolveAllSegments,
166
- resolveInterceptEntry,
167
- createHandleStore,
168
- } = getRouterContext<TEnv>();
163
+ // Read synchronously in the pipeline: the proactive write runs in
164
+ // waitUntil, where the router-context ALS may be gone.
165
+ const routerCtx = getRouterContext<TEnv>();
169
166
 
170
167
  // On a fresh intercept miss the intercept slot segments flow through
171
168
  // `source` into allSegments AND are also recorded on state.interceptSegments.
@@ -178,18 +175,21 @@ export function withCacheStore<TEnv>(
178
175
  ...state.interceptSegments.filter((s) => !seenSegmentIds.has(s.id)),
179
176
  ];
180
177
 
178
+ const cacheScope = ctx.cacheScope;
179
+
180
+ // Only the segments the entry stores count: a null layout above the
181
+ // boundary (the client kept it) is never written, so it needs no re-render.
181
182
  const hasNullComponents = allSegmentsToCache.some(
182
183
  (s) =>
183
184
  s.component === null &&
184
185
  s.type !== "loader" &&
185
- ctx.clientSegmentSet.has(s.id),
186
+ ctx.clientSegmentSet.has(s.id) &&
187
+ cacheScope.covers(s.id, s.namespace),
186
188
  );
187
189
 
188
190
  const requestCtx = getRequestContext();
189
191
  if (!requestCtx) return;
190
192
 
191
- const cacheScope = ctx.cacheScope;
192
-
193
193
  // Record the route's segment-DSL cache tags into the request tag union NOW,
194
194
  // synchronously in the pipeline. The actual store write (cacheRoute) runs in
195
195
  // requestCtx.waitUntil() below — and the proactive path re-resolves the whole
@@ -216,91 +216,21 @@ export function withCacheStore<TEnv>(
216
216
 
217
217
  if (hasNullComponents) {
218
218
  requestCtx.waitUntil(async () => {
219
- const savedMetrics = ctx.Store.metrics;
220
- ctx.Store.metrics = undefined;
221
-
222
219
  const start = performance.now();
223
220
  debugLog("cacheStore", "proactive caching started", {
224
221
  pathname: ctx.pathname,
225
222
  });
226
- const originalHandleStore = requestCtx._handleStore;
227
- requestCtx._handleStore = createHandleStore();
228
223
  try {
229
- const proactiveHandlerContext = createHandlerContext(
230
- ctx.matched.params,
231
- ctx.request,
232
- ctx.url.searchParams,
233
- ctx.pathname,
234
- ctx.url,
235
- ctx.env,
236
- ctx.routeMap,
237
- ctx.matched.routeKey,
238
- ctx.matched.responseType,
239
- ctx.matched.pt === true,
240
- );
241
- const proactiveLoaderPromises = new Map<string, Promise<any>>();
242
-
243
- setupLoaderAccess(proactiveHandlerContext, proactiveLoaderPromises);
244
-
245
- const Store = ctx.Store;
246
- // Re-establish the request-context ALS around the re-render. Store
247
- // is a different ALS (DSL build context); on workerd a waitUntil
248
- // task runs detached from the request's I/O context, so a handler/
249
- // component that reads the ambient getRequestContext() during this
250
- // background re-render would otherwise throw "called outside of a
251
- // request context".
252
- const freshSegments = await runWithRequestContext(requestCtx, () =>
253
- Store.run(() =>
254
- resolveAllSegments(
255
- ctx.entries,
256
- ctx.routeKey,
257
- ctx.matched.params,
258
- proactiveHandlerContext,
259
- proactiveLoaderPromises,
260
- { skipLoaders: true },
261
- ),
262
- ),
263
- );
264
-
265
- let freshInterceptSegments: ResolvedSegment[] = [];
266
- if (ctx.interceptResult) {
267
- freshInterceptSegments = await runWithRequestContext(
268
- requestCtx,
269
- () =>
270
- Store.run(() =>
271
- resolveInterceptEntry(
272
- ctx.interceptResult!.intercept,
273
- ctx.interceptResult!.entry,
274
- ctx.matched.params,
275
- proactiveHandlerContext,
276
- true, // belongsToRoute
277
- // No revalidationContext = render fresh
278
- undefined,
279
- // Skip intercept middleware: the foreground already ran it
280
- // before the response was sent. Re-running here (post-
281
- // response, background) would fire side effects twice and a
282
- // short-circuit Response would silently abort this write.
283
- { skipMiddleware: true },
284
- ),
285
- ),
286
- );
287
- }
288
-
289
- const completeSegments = [
290
- ...freshSegments,
291
- ...freshInterceptSegments,
292
- ];
293
- requestCtx._handleStore.seal();
294
- await cacheScope.cacheRoute(
295
- ctx.pathname,
296
- ctx.matched.params,
297
- completeSegments,
298
- ctx.isIntercept,
224
+ const count = await rerenderAndCacheRoute<TEnv>(
225
+ ctx,
226
+ requestCtx as RequestContext<TEnv>,
227
+ cacheScope,
228
+ routerCtx,
299
229
  );
300
230
  if (INTERNAL_RANGO_DEBUG) {
301
231
  const dur = performance.now() - start;
302
232
  console.log(
303
- `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${completeSegments.length}`,
233
+ `[RSC Background][req:${reqId}] Proactive cache ${ctx.pathname} (${dur.toFixed(2)}ms) segments=${count}`,
304
234
  );
305
235
  }
306
236
  debugLog("cacheStore", "proactive caching complete", {
@@ -317,9 +247,6 @@ export function withCacheStore<TEnv>(
317
247
  pathname: ctx.pathname,
318
248
  error: String(error),
319
249
  });
320
- } finally {
321
- requestCtx._handleStore = originalHandleStore;
322
- ctx.Store.metrics = savedMetrics;
323
250
  }
324
251
  });
325
252
  } else {
@@ -4,7 +4,7 @@ import type {
4
4
  DefaultRouteName,
5
5
  DefaultVars,
6
6
  } from "../types/global-namespace.js";
7
- import type { ScopedReverseFunction } from "../reverse.js";
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";
@@ -74,10 +74,8 @@ export interface MiddlewareContext<
74
74
 
75
75
  setLocationState(entries: LocationStateEntry | LocationStateEntry[]): void;
76
76
 
77
- reverse: ScopedReverseFunction<
78
- Record<string, string>,
79
- DefaultReverseRouteMap
80
- >;
77
+ /** Global route names only; see {@link GlobalReverseFunction}. */
78
+ reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
81
79
  }
82
80
 
83
81
  export type MiddlewareFn<
@@ -1,6 +1,6 @@
1
1
  /// <reference types="vite/types/importMeta.d.ts" />
2
2
 
3
- import { contextGet, contextSet } from "../context-var.js";
3
+ import { contextGet, contextSet, isNonCacheable } from "../context-var.js";
4
4
  import { escapeRegExp } from "../regex-escape.js";
5
5
  import { parsePattern as parseRoutePattern } from "./pattern-matching.js";
6
6
  import { safeDecodeURIComponent } from "./url-params.js";
@@ -22,8 +22,15 @@ import {
22
22
  import { isAutoGeneratedRouteName } from "../route-name.js";
23
23
  import { appendMetric, createMetricsStore } from "./metrics.js";
24
24
  import { observePhase, PHASES } from "./instrument.js";
25
- import { stripInternalParams } from "./handler-context.js";
25
+ import { guardHeaderWrites, stripInternalParams } from "./handler-context.js";
26
26
  import { isWebSocketUpgradeResponse } from "../response-utils.js";
27
+ import {
28
+ INSIDE_CACHE_EXEC,
29
+ NOCACHE_SYMBOL,
30
+ assertNotInsideCacheExec,
31
+ } from "../cache/taint.js";
32
+ import { getCacheExecScope } from "../cache/cache-exec-scope.js";
33
+ import { assertNonCacheableReadAllowed } from "../server/context.js";
27
34
 
28
35
  // Re-export types consumed through this module's path.
29
36
  export type {
@@ -230,7 +237,12 @@ export function createMiddlewareContext<TEnv>(
230
237
  // so snapshotting beats re-reading ALS on every access. The lazy getters
231
238
  // below (routeName, theme, setTheme) stay lazy because those can change
232
239
  // during `await next()`.
233
- return {
240
+ //
241
+ // Request-scoped like the handler ctx (issue #940): the NOCACHE_SYMBOL brand
242
+ // below makes a "use cache" function taking it key by host, _routeName,
243
+ // pathname, params and search; set/header/headers throw inside that
244
+ // function, and non-cacheable get reads run the shared guard.
245
+ const ctx: MiddlewareContext<TEnv> & { readonly _routeName?: string } = {
234
246
  request,
235
247
  url,
236
248
  originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
@@ -254,17 +266,43 @@ export function createMiddlewareContext<TEnv>(
254
266
  ) as MiddlewareContext<TEnv>["routeName"];
255
267
  },
256
268
 
269
+ // The matched route key, auto-generated names included (the handler
270
+ // ctx's _routeName); undefined before routing.
271
+ get _routeName(): string | undefined {
272
+ return _getRequestContext()?._routeName;
273
+ },
274
+
275
+ // Guarded only inside a "use cache" body: the stamp covers a miss that
276
+ // took this ctx as an argument, the exec scope a stale refresh (which does
277
+ // not stamp its args, cache-runtime.ts). A write there is lost on a hit.
257
278
  get headers(): Headers {
258
- return getResponse().headers;
279
+ const headers = getResponse().headers;
280
+ return (INSIDE_CACHE_EXEC as symbol) in ctx ||
281
+ getCacheExecScope() !== undefined
282
+ ? guardHeaderWrites(headers, () =>
283
+ assertNotInsideCacheExec(ctx, "headers"),
284
+ )
285
+ : headers;
259
286
  },
260
287
 
261
- get: ((keyOrVar: any) =>
262
- contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
288
+ get: ((keyOrVar: any) => {
289
+ // "use cache" only: intercept middleware runs inside a cache()
290
+ // subtree's render scope, and a middleware read is never cached there.
291
+ if (
292
+ isNonCacheable(variables, keyOrVar) &&
293
+ getCacheExecScope() !== undefined
294
+ ) {
295
+ assertNonCacheableReadAllowed(keyOrVar);
296
+ }
297
+ return contextGet(variables, keyOrVar);
298
+ }) as MiddlewareContext<TEnv>["get"],
263
299
 
264
300
  set: ((keyOrVar: any, value: unknown, options?: any) => {
301
+ assertNotInsideCacheExec(ctx, "set");
265
302
  contextSet(variables, keyOrVar, value, options);
266
303
  }) as MiddlewareContext<TEnv>["set"],
267
304
  header(name: string, value: string): void {
305
+ assertNotInsideCacheExec(ctx, "header");
268
306
  // Before next(): delegate to shared RequestContext stub
269
307
  if (isPreNext()) {
270
308
  const reqCtx = _getRequestContext();
@@ -323,6 +361,8 @@ export function createMiddlewareContext<TEnv>(
323
361
  }
324
362
  },
325
363
  };
364
+ (ctx as any)[NOCACHE_SYMBOL] = true;
365
+ return ctx;
326
366
  }
327
367
 
328
368
  /**
@@ -852,6 +892,7 @@ export function collectRouteMiddleware(
852
892
  params: Record<string, string>,
853
893
  ): CollectedMiddleware[] {
854
894
  const result: CollectedMiddleware[] = [];
895
+ const chain = [...entries];
855
896
 
856
897
  const collect = (entry: MiddlewareCollectableEntry): void => {
857
898
  // Collect entry's own middleware
@@ -860,15 +901,17 @@ export function collectRouteMiddleware(
860
901
  result.push({ handler: mw, params });
861
902
  }
862
903
  }
863
- // Collect middleware from orphan layouts (recursive)
904
+ // Collect middleware from orphan layouts (recursive). A bare cache()
905
+ // marker is both its layout's orphan and a chain entry of the routes
906
+ // after it; it is collected once, at its chain position (issue #918).
864
907
  if (entry.layout && entry.layout.length > 0) {
865
908
  for (const orphan of entry.layout) {
866
- collect(orphan);
909
+ if (!chain.includes(orphan)) collect(orphan);
867
910
  }
868
911
  }
869
912
  };
870
913
 
871
- for (const entry of entries) {
914
+ for (const entry of chain) {
872
915
  collect(entry);
873
916
  }
874
917
 
@@ -15,6 +15,7 @@ import {
15
15
  import { detectPrerenderPassthrough } from "../prerender.js";
16
16
  import { isRouteRootScoped } from "../route-map-builder.js";
17
17
  import { setupBuildUse } from "./loader-resolution.js";
18
+ import { findInOrphans } from "./error-handling.js";
18
19
  import { loadManifest } from "./manifest.js";
19
20
  import { traverseBack } from "./pattern-matching.js";
20
21
  import type { RouterContext } from "./router-context.js";
@@ -306,7 +307,18 @@ export async function matchForPrerender<TEnv = any>(
306
307
  // 12. Serialize segments using the cache serializer
307
308
  const { serializeSegments } = await import("../cache/segment-codec.js");
308
309
  const { encodeHandles } = await import("../cache/handle-snapshot.js");
309
- const serializedSegments = await serializeSegments(nonLoaderSegments);
310
+ // Flight encodes a component that throws (an async server component in
311
+ // the tree) as an error row and completes normally. Rethrown below like
312
+ // a handler throw, so the caller's policy decides instead of baking the
313
+ // error (#914).
314
+ const flightErrors: unknown[] = [];
315
+ const onFlightError = (error: unknown): void => {
316
+ flightErrors.push(error);
317
+ };
318
+ const serializedSegments = await serializeSegments(
319
+ nonLoaderSegments,
320
+ onFlightError,
321
+ );
310
322
 
311
323
  // 13. Collect handle data per segment (skip segments with no handle data).
312
324
  // Encoded through the Flight codec (not stored raw) so Promise/ReactNode
@@ -322,7 +334,9 @@ export async function matchForPrerender<TEnv = any>(
322
334
  handlesRecord[seg.id] = await resolveSegmentHandleValues(segHandles);
323
335
  }
324
336
  }
325
- const handles = await encodeHandles(handlesRecord);
337
+ const handles = await encodeHandles(handlesRecord, onFlightError);
338
+ // Before intercept resolution, as a handler throw would be.
339
+ if (flightErrors.length > 0) throw flightErrors[0];
326
340
 
327
341
  // Use the trie-level route key (e.g., "docs", "docs.article")
328
342
  const routeName = matched.routeKey;
@@ -350,19 +364,23 @@ export async function matchForPrerender<TEnv = any>(
350
364
  intercept: InterceptEntry;
351
365
  entry: EntryData;
352
366
  }[] = [];
353
- let current: EntryData | null = manifestEntry;
354
- while (current) {
355
- // Flatten the entry and its sibling layouts into one source list, the
356
- // same traversal findInterceptForRoute uses; the build keeps ALL matches
357
- // (not just the innermost) and skips when(). intercept/layout are
358
- // non-optional arrays, so empty ones are a no-op here.
359
- for (const source of [current, ...current.layout]) {
360
- for (const ic of source.intercept) {
361
- if (ic.routeName === matched.routeKey) {
362
- foundIntercepts.push({ intercept: ic, entry: source });
363
- }
367
+ // The entry, then its orphans (nested ones included): the same traversal
368
+ // findInterceptForRoute uses; the build keeps ALL matches (not just the
369
+ // innermost) and skips when().
370
+ const collect = (source: EntryData): undefined => {
371
+ for (const ic of source.intercept) {
372
+ if (ic.routeName === matched.routeKey) {
373
+ foundIntercepts.push({ intercept: ic, entry: source });
364
374
  }
365
375
  }
376
+ return undefined;
377
+ };
378
+ let current: EntryData | null = manifestEntry;
379
+ let prev: EntryData | null = null;
380
+ while (current) {
381
+ collect(current);
382
+ findInOrphans(current, collect, prev);
383
+ prev = current;
366
384
  current = current.parent;
367
385
  }
368
386
 
@@ -427,6 +445,7 @@ export async function matchForPrerender<TEnv = any>(
427
445
  await handleStore.settled;
428
446
  interceptSegments = await serializeSegments(
429
447
  interceptResolvedSegments,
448
+ onFlightError,
430
449
  );
431
450
  const interceptHandlesRecord: Record<string, SegmentHandleData> = {};
432
451
  for (const seg of interceptResolvedSegments) {
@@ -439,13 +458,18 @@ export async function matchForPrerender<TEnv = any>(
439
458
  // The intercept artifact serves main + intercept segments together, so
440
459
  // encode the MERGED handle map here (the sinks no longer merge raw
441
460
  // records — they store this pre-encoded string as-is).
442
- interceptHandles = await encodeHandles({
443
- ...handlesRecord,
444
- ...interceptHandlesRecord,
445
- });
461
+ interceptHandles = await encodeHandles(
462
+ {
463
+ ...handlesRecord,
464
+ ...interceptHandlesRecord,
465
+ },
466
+ onFlightError,
467
+ );
446
468
  }
447
469
  }
448
470
 
471
+ if (flightErrors.length > 0) throw flightErrors[0];
472
+
449
473
  return {
450
474
  segments: serializedSegments,
451
475
  handles,
@@ -576,7 +600,12 @@ export async function renderStaticSegment<TEnv = any>(
576
600
 
577
601
  const { serializeSegments } = await import("../cache/segment-codec.js");
578
602
  const { encodeHandleValue } = await import("../cache/handle-snapshot.js");
579
- const [serialized] = await serializeSegments([segment]);
603
+ // Same error-row guard as matchForPrerender (#914).
604
+ const flightErrors: unknown[] = [];
605
+ const onFlightError = (error: unknown): void => {
606
+ flightErrors.push(error);
607
+ };
608
+ const [serialized] = await serializeSegments([segment], onFlightError);
580
609
 
581
610
  // Collect handle data pushed during rendering and Flight-encode it (so
582
611
  // Promise/ReactNode handle values survive the JSON build artifact). "" when
@@ -584,9 +613,14 @@ export async function renderStaticSegment<TEnv = any>(
584
613
  const segHandles = handleStore.getDataForSegment(handlerId);
585
614
  const handles =
586
615
  Object.keys(segHandles).length > 0
587
- ? await encodeHandleValue(await resolveSegmentHandleValues(segHandles))
616
+ ? await encodeHandleValue(
617
+ await resolveSegmentHandleValues(segHandles),
618
+ onFlightError,
619
+ )
588
620
  : "";
589
621
 
622
+ if (flightErrors.length > 0) throw flightErrors[0];
623
+
590
624
  return { encoded: serialized.encoded, handles };
591
625
  });
592
626
  }
@@ -15,6 +15,7 @@
15
15
  import type { CacheScope } from "../cache/cache-scope.js";
16
16
  import { createCacheScope } from "../cache/cache-scope.js";
17
17
  import type { EntryData, MetricsStore } from "../server/context.js";
18
+ import { isPprEntry } from "../server/context.js";
18
19
  import { loadManifest } from "./manifest.js";
19
20
  import { collectRouteMiddleware } from "./middleware.js";
20
21
  import type { CollectedMiddleware } from "./middleware-types.js";
@@ -222,10 +223,19 @@ function buildEntriesAndCacheScope(manifestEntry: EntryData): {
222
223
  cacheScope: CacheScope | null;
223
224
  } {
224
225
  const entries = [...traverseBack(manifestEntry)];
226
+ // A cache() boundary keeps the entries above it live. A ppr route is a
227
+ // document-scoped cache(): its whole chain bakes into the shell, and the
228
+ // shell HIT tail must replay it to match the prelude, so its scope covers
229
+ // the whole chain.
230
+ const wholeChain = isPprEntry(manifestEntry);
225
231
  let cacheScope: CacheScope | null = null;
226
232
  for (const entry of entries) {
227
233
  if (entry.cache) {
228
- cacheScope = createCacheScope(entry.cache, cacheScope);
234
+ cacheScope = createCacheScope(
235
+ entry.cache,
236
+ cacheScope,
237
+ wholeChain ? undefined : entry.shortCode,
238
+ );
229
239
  }
230
240
  }
231
241
  return { entries, cacheScope };
@@ -131,6 +131,8 @@ export interface RouterContext<TEnv = any> {
131
131
  localRouteName: string,
132
132
  pathname: string,
133
133
  stale?: boolean,
134
+ /** The matched route's full chain when `entries` is part of it (ResolveSegmentOptions.chain). */
135
+ chain?: readonly EntryData[],
134
136
  ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
135
137
 
136
138
  // Generator-based segment resolution (for pipeline)
@@ -177,6 +177,23 @@ export interface Rango<
177
177
  */
178
178
  readonly routeMap: TRoutes;
179
179
 
180
+ /**
181
+ * Serialize the route manifest for inspection: every route and layout the
182
+ * router's `.routes()` / `urls` mounts register, with shortCodes, parent
183
+ * links, and loader/middleware/error-boundary/parallel/intercept flags.
184
+ *
185
+ * Server-only development aid. It re-evaluates each mount's `urls()` tree.
186
+ * Groups mounted with `include()` are lazy and are not expanded: their
187
+ * routes are absent from the result.
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * const manifest = await router.debugManifest();
192
+ * console.log(manifest.totalRoutes, Object.keys(manifest.routes));
193
+ * ```
194
+ */
195
+ debugManifest(): Promise<SerializedManifest>;
196
+
180
197
  /**
181
198
  * Handle an RSC request.
182
199
  *
@@ -187,7 +204,13 @@ export interface Rango<
187
204
  * ```tsx
188
205
  * import { router } from "./router";
189
206
  *
190
- * export default { fetch: router.fetch };
207
+ * // Workers call fetch(request, env, ctx); `{ fetch: router.fetch }` would
208
+ * // drop env and ctx.
209
+ * export default {
210
+ * fetch(request, env, ctx) {
211
+ * return router.fetch(request, { env, ctx });
212
+ * },
213
+ * } satisfies ExportedHandler;
191
214
  * ```
192
215
  *
193
216
  * @example Direct export
@@ -316,7 +339,7 @@ export interface RangoInternal<
316
339
 
317
340
  /**
318
341
  * Resolved theme configuration (null if theme not enabled)
319
- * Used by NavigationProvider to include ThemeProvider and by MetaTags to render theme script
342
+ * Used by NavigationProvider to include ThemeProvider and by `Html.Meta` to render theme script
320
343
  */
321
344
  readonly themeConfig: import("../theme/types.js").ResolvedThemeConfig | null;
322
345
 
@@ -164,8 +164,10 @@ export interface RangoOptions<TEnv = any> {
164
164
  *
165
165
  * Must be a client component ("use client") that accepts { children }.
166
166
  *
167
- * If not provided, a default document with basic HTML structure is used:
168
- * `<html><head><meta charset/viewport></head><body>{children}</body></html>`
167
+ * If not provided, a default document is used: `Html.Meta` and `Html.Scripts`
168
+ * in `<head>`, `Html.Scripts position="body"` before `{children}` and
169
+ * `Html.ScrollRestoration` after it. Pass your own to set `getKey` or to
170
+ * leave back/forward scrolling to the browser.
169
171
  *
170
172
  * @example
171
173
  * ```typescript
@@ -201,7 +203,8 @@ export interface RangoOptions<TEnv = any> {
201
203
 
202
204
  /**
203
205
  * Default not-found boundary fallback used when no notFoundBoundary is defined in the route tree
204
- * If not provided, DataNotFoundError will be treated as a regular error
206
+ * If not provided, notFound() renders the `notFound` option, else a plain
207
+ * `<h1>Not Found</h1>` (status 404)
205
208
  */
206
209
  defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler;
207
210
 
@@ -211,7 +214,7 @@ export interface RangoOptions<TEnv = any> {
211
214
  * This is rendered within your document/app shell with a 404 status code.
212
215
  * Use this for a custom 404 page that maintains your app's look and feel.
213
216
  *
214
- * If not provided, a default "Page not found" component is rendered.
217
+ * If not provided, a plain `<h1>Not Found</h1>` is rendered.
215
218
  *
216
219
  * Can be a static ReactNode or a function receiving the pathname.
217
220
  *
@@ -368,7 +371,7 @@ export interface RangoOptions<TEnv = any> {
368
371
  * When provided, enables:
369
372
  * - ctx.theme and ctx.setTheme() in route handlers
370
373
  * - useTheme() hook for client components
371
- * - FOUC prevention via inline script in MetaTags
374
+ * - FOUC prevention via inline script in `<Html.Meta />`
372
375
  * - Automatic ThemeProvider wrapping in NavigationProvider
373
376
  *
374
377
  * @example