@rangojs/router 0.16.0 → 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 (245) 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 +7 -4
  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 +33 -9
  32. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  33. package/dist/types/route-types.d.ts +1 -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/types/cache-types.d.ts +14 -10
  67. package/dist/types/types/handler-context.d.ts +14 -0
  68. package/dist/types/types/loader-types.d.ts +8 -6
  69. package/dist/types/urls/response-types.d.ts +3 -12
  70. package/dist/types/use-loader.d.ts +12 -3
  71. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  72. package/dist/types/vite/rango.d.ts +1 -1
  73. package/dist/vite/index.js +1381 -1346
  74. package/package.json +2 -2
  75. package/skills/api-client/SKILL.md +22 -10
  76. package/skills/breadcrumbs/SKILL.md +65 -22
  77. package/skills/bundle-analysis/SKILL.md +22 -22
  78. package/skills/cache-guide/SKILL.md +207 -97
  79. package/skills/caching/SKILL.md +295 -93
  80. package/skills/catalog.json +16 -16
  81. package/skills/client-urls/SKILL.md +53 -31
  82. package/skills/cloudflare/SKILL.md +72 -20
  83. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  84. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  85. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  86. package/skills/comparison/SKILL.md +5 -0
  87. package/skills/comparison/references/framework-comparison.md +39 -33
  88. package/skills/composability/SKILL.md +38 -29
  89. package/skills/css/SKILL.md +10 -13
  90. package/skills/debug-manifest/SKILL.md +74 -31
  91. package/skills/defer-hydration/SKILL.md +2 -1
  92. package/skills/deployment-caching/SKILL.md +6 -1
  93. package/skills/document-cache/SKILL.md +92 -28
  94. package/skills/fonts/SKILL.md +16 -8
  95. package/skills/handler-use/SKILL.md +53 -17
  96. package/skills/hooks/SKILL.md +28 -20
  97. package/skills/hooks/data.md +23 -3
  98. package/skills/hooks/handle-and-actions.md +31 -16
  99. package/skills/hooks/navigation.md +39 -10
  100. package/skills/hooks/outlets.md +13 -6
  101. package/skills/hooks/state.md +37 -15
  102. package/skills/hooks/urls.md +7 -5
  103. package/skills/host-router/SKILL.md +64 -12
  104. package/skills/i18n/SKILL.md +60 -34
  105. package/skills/intercept/SKILL.md +161 -26
  106. package/skills/layout/SKILL.md +106 -50
  107. package/skills/links/SKILL.md +51 -19
  108. package/skills/loader/SKILL.md +242 -106
  109. package/skills/middleware/SKILL.md +119 -23
  110. package/skills/migrate-nextjs/SKILL.md +100 -45
  111. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  112. package/skills/migrate-react-router/SKILL.md +49 -38
  113. package/skills/migrate-react-router/component-migration.md +25 -6
  114. package/skills/migrate-react-router/data-and-actions.md +18 -5
  115. package/skills/migrate-react-router/route-mapping.md +12 -6
  116. package/skills/mime-routes/SKILL.md +12 -7
  117. package/skills/observability/SKILL.md +28 -14
  118. package/skills/parallel/SKILL.md +75 -39
  119. package/skills/ppr/SKILL.md +345 -150
  120. package/skills/prerender/SKILL.md +189 -100
  121. package/skills/rango/SKILL.md +65 -34
  122. package/skills/react-compiler/SKILL.md +6 -5
  123. package/skills/response-routes/SKILL.md +133 -25
  124. package/skills/route/SKILL.md +165 -60
  125. package/skills/router-setup/SKILL.md +418 -240
  126. package/skills/scripts/SKILL.md +31 -20
  127. package/skills/server-actions/SKILL.md +220 -59
  128. package/skills/shell-manifest/SKILL.md +12 -7
  129. package/skills/streams-and-websockets/SKILL.md +32 -14
  130. package/skills/tailwind/SKILL.md +37 -33
  131. package/skills/testing/SKILL.md +68 -32
  132. package/skills/testing/cache-prerender.md +55 -29
  133. package/skills/testing/client-components.md +59 -6
  134. package/skills/testing/e2e-parity.md +12 -2
  135. package/skills/testing/flight.md +19 -11
  136. package/skills/testing/handles.md +8 -5
  137. package/skills/testing/loader.md +8 -5
  138. package/skills/testing/middleware.md +8 -3
  139. package/skills/testing/predicates.md +120 -0
  140. package/skills/testing/render-handler.md +70 -22
  141. package/skills/testing/response-routes.md +11 -4
  142. package/skills/testing/reverse-and-types.md +20 -14
  143. package/skills/testing/server-actions.md +12 -3
  144. package/skills/testing/server-tree.md +9 -0
  145. package/skills/testing/setup.md +9 -5
  146. package/skills/theme/SKILL.md +106 -29
  147. package/skills/typesafety/SKILL.md +20 -9
  148. package/skills/typesafety/env-and-bindings.md +126 -47
  149. package/skills/typesafety/generated-files-and-cli.md +26 -14
  150. package/skills/typesafety/params-and-search.md +44 -19
  151. package/skills/typesafety/route-types.md +50 -14
  152. package/skills/use-cache/SKILL.md +200 -82
  153. package/skills/vercel/SKILL.md +21 -11
  154. package/skills/view-transitions/SKILL.md +41 -20
  155. package/src/__augment-tests__/augmented.check.ts +26 -1
  156. package/src/browser/index.ts +2 -2
  157. package/src/browser/navigation-bridge.ts +3 -4
  158. package/src/browser/navigation-transaction.ts +22 -2
  159. package/src/browser/partial-update.ts +35 -25
  160. package/src/browser/react/ScrollRestoration.tsx +6 -6
  161. package/src/browser/rsc-router.tsx +1 -1
  162. package/src/browser/scroll-restoration.ts +53 -6
  163. package/src/cache/cache-exec-scope.ts +37 -4
  164. package/src/cache/cache-key-utils.ts +1 -6
  165. package/src/cache/cache-runtime.ts +230 -61
  166. package/src/cache/cache-scope.ts +76 -12
  167. package/src/cache/document-cache.ts +15 -0
  168. package/src/cache/handle-capture.ts +90 -10
  169. package/src/cache/handle-snapshot.ts +132 -28
  170. package/src/cache/segment-codec.ts +26 -7
  171. package/src/cache/tag-invalidation.ts +4 -3
  172. package/src/cache/taint.ts +1 -1
  173. package/src/cache/types.ts +16 -3
  174. package/src/client.rsc.tsx +10 -9
  175. package/src/client.tsx +5 -4
  176. package/src/components/DefaultDocument.tsx +7 -2
  177. package/src/context-var.ts +6 -5
  178. package/src/escape-script.ts +1 -1
  179. package/src/handles/MetaTags.tsx +13 -11
  180. package/src/handles/Scripts.tsx +11 -10
  181. package/src/handles/meta.ts +2 -2
  182. package/src/handles/script.ts +17 -16
  183. package/src/href-client.ts +1 -1
  184. package/src/html.ts +12 -0
  185. package/src/index.rsc.ts +5 -0
  186. package/src/index.ts +1 -0
  187. package/src/loader-store.ts +7 -4
  188. package/src/prerender/build-shell-capture.ts +8 -11
  189. package/src/reverse.ts +44 -4
  190. package/src/route-definition/dsl-helpers.ts +117 -30
  191. package/src/route-definition/helpers-types.ts +33 -9
  192. package/src/route-definition/resolve-handler-use.ts +121 -6
  193. package/src/route-types.ts +0 -3
  194. package/src/router/debug-manifest.ts +10 -2
  195. package/src/router/error-handling.ts +51 -15
  196. package/src/router/handler-context.ts +34 -25
  197. package/src/router/intercept-resolution.ts +125 -39
  198. package/src/router/loader-resolution.ts +56 -11
  199. package/src/router/match-api.ts +12 -17
  200. package/src/router/match-middleware/background-revalidation.ts +161 -96
  201. package/src/router/match-middleware/cache-lookup.ts +93 -10
  202. package/src/router/match-middleware/cache-store.ts +17 -90
  203. package/src/router/middleware-types.ts +3 -5
  204. package/src/router/middleware.ts +52 -9
  205. package/src/router/prerender-match.ts +53 -19
  206. package/src/router/route-snapshot.ts +11 -1
  207. package/src/router/router-context.ts +2 -0
  208. package/src/router/router-interfaces.ts +25 -2
  209. package/src/router/router-options.ts +8 -5
  210. package/src/router/segment-resolution/fresh.ts +44 -12
  211. package/src/router/segment-resolution/loader-cache.ts +205 -45
  212. package/src/router/segment-resolution/loader-mask.ts +12 -22
  213. package/src/router/segment-resolution/loader-snapshot.ts +6 -6
  214. package/src/router/segment-resolution/mask-nested.ts +7 -7
  215. package/src/router/segment-resolution/revalidation.ts +34 -6
  216. package/src/router/segment-wrappers.ts +4 -0
  217. package/src/rsc/render-pipeline.ts +3 -0
  218. package/src/rsc/response-route-handler.ts +11 -3
  219. package/src/rsc/rsc-rendering.ts +8 -45
  220. package/src/rsc/shell-capture-constants.ts +13 -0
  221. package/src/rsc/shell-capture.ts +111 -90
  222. package/src/rsc/ssr-setup.ts +14 -8
  223. package/src/rsc/types.ts +11 -0
  224. package/src/server/context.ts +88 -11
  225. package/src/server/cookie-store.ts +5 -4
  226. package/src/server/handle-store.ts +232 -7
  227. package/src/server/request-context.ts +35 -30
  228. package/src/ssr/index.tsx +40 -7
  229. package/src/static-handler.ts +4 -1
  230. package/src/testing/flight-runtime.d.ts +21 -0
  231. package/src/testing/generated-routes.ts +2 -2
  232. package/src/testing/internal/context.ts +1 -1
  233. package/src/testing/internal/flight-client-globals.ts +7 -5
  234. package/src/testing/render-route.tsx +85 -24
  235. package/src/testing/run-loader.ts +5 -1
  236. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  237. package/src/testing/vitest.ts +62 -1
  238. package/src/theme/ThemeScript.tsx +3 -3
  239. package/src/types/cache-types.ts +14 -10
  240. package/src/types/handler-context.ts +14 -0
  241. package/src/types/loader-types.ts +8 -6
  242. package/src/urls/response-types.ts +3 -23
  243. package/src/use-loader.tsx +12 -3
  244. package/src/vite/plugins/use-cache-transform.ts +118 -74
  245. package/src/vite/rango.ts +7 -3
@@ -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
@@ -120,12 +120,8 @@ export async function resolveLoaders<TEnv>(
120
120
  // loading-disabled entries consult it), so normal requests are unchanged.
121
121
  const emitStreaming = !loadingDisabled || isShellCaptureActive();
122
122
 
123
- // PPR lane decision for this entry's loaders (loader-container-bake): an
124
- // entry WITHOUT renderable loading() puts its loaders on the BAKE lane —
125
- // executed at capture (container bakes, nested pending promises hole at the
126
- // consumer's Suspense) and overlay-pinned from the shell snapshot on a HIT.
127
- // Renderable loading() keeps the LIVE lane (masked at capture, always
128
- // fresh). Computed per entry; resolveLoaderData applies the policy.
123
+ // PPR bake/seed key input only; lane rule: see resolveLoaderData
124
+ // (loader-cache.ts). Unflagged loaders mask at capture regardless of the key.
129
125
  const bakeLane = !entryLoadingMasksLoaders(entry.loading);
130
126
 
131
127
  // Error context for wrapLoaderPromise: without it, a throwing DSL loader never
@@ -146,9 +142,9 @@ export async function resolveLoaders<TEnv>(
146
142
  // barrier), and the loader body can call rendered() before the await below
147
143
  // is reached.
148
144
  //
149
- // Shell-capture renders await too: flagged loaders BAKE at capture
150
- // (loader-mask masks only LIVE-lane loaders). Capture must await them so
151
- // bake-lane handle pushes land in the prelude.
145
+ // Shell-capture renders await too: flagged loaders execute at capture
146
+ // (lane rule: see resolveLoaderData, loader-cache.ts), so their handle
147
+ // pushes must land in the prelude.
152
148
  const awaitedIndices = registerAwaitBeforeFlushIds(loaderEntries);
153
149
 
154
150
  // Streaming loaders: promises kick off now, settle during RSC serialization.
@@ -276,6 +272,16 @@ export interface ResolveSegmentOptions {
276
272
  * so error boundaries keep catching at request time.
277
273
  */
278
274
  throwOnError?: boolean;
275
+ /**
276
+ * The matched route's full entry chain. resolveAllSegments defaults it to
277
+ * its `entries`; withCacheLookup passes it when it resolves only the entries
278
+ * above a cache() boundary. An orphan list can hold an entry of the chain: a
279
+ * bare cache() marker sits in its layout's layout[] (it wraps that layout's
280
+ * routes) and is the parent of the routes after it. For those routes it
281
+ * resolves once, as a chain entry, below the cache() header latch (issue
282
+ * #918: resolved twice on a MISS, once and discarded on a HIT).
283
+ */
284
+ chain?: readonly EntryData[];
279
285
  }
280
286
 
281
287
  /**
@@ -349,6 +355,7 @@ export async function resolveSegment<TEnv>(
349
355
  }
350
356
 
351
357
  for (const orphan of entry.layout) {
358
+ if (options?.chain?.includes(orphan)) continue;
352
359
  const orphanSegments = await resolveOrphanLayout(
353
360
  orphan,
354
361
  params,
@@ -578,6 +585,26 @@ export async function resolveOrphanLayout<TEnv>(
578
585
  resolvedParallelEntries.add(parallelEntry.id);
579
586
  }
580
587
 
588
+ // Routeless entries nested in this one (a layout() after a bare cache()
589
+ // marker, a wrapper inside a routeless wrapper) wrap the same content one
590
+ // level deeper. Issue #918: they were never rendered. None is in the
591
+ // matched chain: that would put this orphan in it, and resolveSegment
592
+ // skips those.
593
+ for (const nested of orphan.layout) {
594
+ segments.push(
595
+ ...(await resolveOrphanLayout(
596
+ nested,
597
+ params,
598
+ context,
599
+ loaderPromises,
600
+ belongsToRoute,
601
+ deps,
602
+ options,
603
+ routeKey,
604
+ )),
605
+ );
606
+ }
607
+
581
608
  return segments;
582
609
  }
583
610
 
@@ -736,6 +763,10 @@ export async function resolveAllSegments<TEnv>(
736
763
  ): Promise<ResolvedSegment[]> {
737
764
  const allSegments: ResolvedSegment[] = [];
738
765
  const seenIds = new Set<string>();
766
+ const segmentOptions: ResolveSegmentOptions = {
767
+ ...options,
768
+ chain: options?.chain ?? entries,
769
+ };
739
770
 
740
771
  // ppr routes are document-scoped cached territory: the whole chain (root
741
772
  // layout down to the page) bakes into the shared shell, so the header-write
@@ -760,8 +791,9 @@ export async function resolveAllSegments<TEnv>(
760
791
  // Set ALS flag when entering a cache() boundary so that ctx.get()
761
792
  // can guard non-cacheable variable reads. Also latch the header-write
762
793
  // scope (response-level side effects — headers/cookies/status).
763
- // Persists for all descendant entries.
764
- if (entry.type === "cache") {
794
+ // Persists for all descendant entries. A route entry carries `cache` when
795
+ // cache() is among its own children (dsl-helpers.ts cache()).
796
+ if (entry.cache) {
765
797
  const store = RangoContext.getStore();
766
798
  if (store) store.insideCacheScope = true;
767
799
  latchCachedHeaderScope("cache", routeKey);
@@ -779,7 +811,7 @@ export async function resolveAllSegments<TEnv>(
779
811
  loaderPromises,
780
812
  deps,
781
813
  false,
782
- options,
814
+ segmentOptions,
783
815
  ),
784
816
  (seg) => [seg],
785
817
  deps,