@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
package/src/reverse.ts CHANGED
@@ -207,6 +207,43 @@ export type ScopedReverseFunction<
207
207
  ): string;
208
208
  };
209
209
 
210
+ /**
211
+ * Reverse function that resolves global route names only: no include() scope,
212
+ * no param auto-fill.
213
+ *
214
+ * This is the `ctx.reverse` of middleware (`MiddlewareContext`) and
215
+ * response-route handlers (`ResponseHandlerContext`). On a live request both
216
+ * get `createReverseFunction(routeMap)` with no route name or params (the
217
+ * middleware callers in rsc/handler.ts and rsc/loader-fetch.ts,
218
+ * handleResponseRoute, router/intercept-resolution.ts; mirrored by
219
+ * testing/run-middleware.ts and testing/dispatch.ts), so a dot-local `.name`
220
+ * has no scope to resolve against and throws `Unknown route`. Build-time PPR
221
+ * shell capture (prerender/build-shell-capture.ts) gives global and route
222
+ * middleware the same global-only reverse.
223
+ *
224
+ * - Route map with a string index signature (no generated map, the permissive
225
+ * `Record<string, string>` fallback): any name except a dot-prefixed literal.
226
+ * `Exclude` is required because `keyof` collapses to `string` there, so the
227
+ * global overloads of `ScopedReverseFunction` would accept `.name` too.
228
+ * - Concrete route map: the global overloads of `ScopedReverseFunction`, with
229
+ * name and param validation. The empty local map (`{}`) leaves no dot-prefixed
230
+ * overload that can match.
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * reverse("blog.post", { slug: "hello" }) // ok: global route + params
235
+ * reverse(".post", { slug: "hello" }) // compile error: no include() scope
236
+ * ```
237
+ */
238
+ export type GlobalReverseFunction<TGlobalRoutes> =
239
+ string extends keyof TGlobalRoutes
240
+ ? <TName extends string>(
241
+ name: Exclude<TName, `.${string}`>,
242
+ params?: Record<string, string>,
243
+ search?: Record<string, unknown>,
244
+ ) => string
245
+ : ScopedReverseFunction<{}, TGlobalRoutes>;
246
+
210
247
  /**
211
248
  * Extract local routes type from UrlPatterns
212
249
  * Used with scopedReverse() to get the routes type from patterns
@@ -290,11 +327,14 @@ export type { RouteResponse } from "./urls.js";
290
327
  * Get a locally-typed reverse function from ctx.reverse for composable modules.
291
328
  *
292
329
  * This is a type-only cast - ctx.reverse already resolves names at runtime.
293
- * Provides type safety: `.name` validates against local routes,
294
- * `name` validates against global named-routes.
330
+ * Only the module's LOCAL routes are typed: `.name` and bare `name` both
331
+ * validate against the local map (the global map parameter defaults to it).
332
+ * Always dot-prefix local names — a bare name type-checks against the local
333
+ * map but resolves as a global name at runtime. For global names, call
334
+ * ctx.reverse directly.
295
335
  *
296
336
  * @param reverse - The ctx.reverse function from HandlerContext
297
- * @returns The same reverse function, typed with local + global routes
337
+ * @returns The same reverse function, typed with the module's local routes
298
338
  *
299
339
  * @example
300
340
  * ```typescript
@@ -305,7 +345,7 @@ export type { RouteResponse } from "./urls.js";
305
345
  *
306
346
  * reverse(".index"); // ✓ Local route
307
347
  * reverse(".post", { slug: "x" }); // ✓ Local with params
308
- * reverse("shop.cart"); // ✓ Global route
348
+ * ctx.reverse("shop.cart"); // Global route: use ctx.reverse
309
349
  *
310
350
  * return <BlogIndex />;
311
351
  * }, { name: "index" }),
@@ -44,7 +44,11 @@ import type {
44
44
  UseItems,
45
45
  } from "../route-types.js";
46
46
  import type { RouteHelpers } from "./helpers-types.js";
47
- import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
47
+ import {
48
+ resolveHandlerUse,
49
+ mergeHandlerUse,
50
+ validateInterceptUseItems,
51
+ } from "./resolve-handler-use.js";
48
52
  import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
49
53
 
50
54
  /**
@@ -131,16 +135,37 @@ const isOrphan = (result: AllUseItems[]): boolean =>
131
135
  * pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
132
136
  * docs/tree-structure.md) and push it onto the parent's layout[] so it renders
133
137
  * as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
134
- * validation and registers inline.
138
+ * validation and registers inline. `orphanOwner` keeps the owner reachable for
139
+ * boundary lookup only (router/error-handling.ts).
135
140
  */
136
141
  const attachOrphanSibling = (
137
142
  parent: EntryData | null,
138
143
  entry: EntryData,
139
144
  ): void => {
140
145
  entry.parent = null;
141
- if (parent && "layout" in parent) parent.layout.push(entry);
146
+ if (parent && "layout" in parent) {
147
+ parent.layout.push(entry);
148
+ entry.orphanOwner = parent;
149
+ }
142
150
  };
143
151
 
152
+ /**
153
+ * The route whose use() list `parent` sits in: `parent` itself, or the route
154
+ * above a chain of layout-typed wrappers (layout(), middleware()/transition()
155
+ * wrapper form). Inside a path those wrappers never hold routes (path-in-path
156
+ * is rejected), so the route is the only cacheable unit there. An intercept()
157
+ * scope (its temporary parent: a spread of the declaring entry with an own
158
+ * `cache` sink) stops the walk, so its cache() stays rejected by
159
+ * validateInterceptUseItems instead of configuring the path around it.
160
+ */
161
+ function enclosingRoute(parent: EntryData | null): EntryData | undefined {
162
+ for (let entry = parent; entry; entry = entry.parent) {
163
+ if (entry.type === "route") return entry;
164
+ if (entry.type !== "layout" || "cache" in entry) return undefined;
165
+ }
166
+ return undefined;
167
+ }
168
+
144
169
  /**
145
170
  * Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
146
171
  * entry that captures the attachments declared by a use() callback — restoring
@@ -179,7 +204,7 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
179
204
  * When an error occurs during rendering of this segment or its children,
180
205
  * the fallback will be rendered instead. The fallback can be:
181
206
  * - A static ReactNode (e.g., <ErrorPage />)
182
- * - A handler function that receives error info and reset function
207
+ * - A handler function that receives `{ error }` (server fallbacks get no reset)
183
208
  *
184
209
  * Error boundaries catch errors from:
185
210
  * - Middleware execution
@@ -195,11 +220,11 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
195
220
  *
196
221
  * // Or with handler for dynamic error UI:
197
222
  * route("products.detail", ProductDetail, () => [
198
- * errorBoundary(({ error, reset }) => (
223
+ * errorBoundary(({ error }) => (
199
224
  * <div>
200
225
  * <h2>Product failed to load</h2>
201
226
  * <p>{error.message}</p>
202
- * <button onClick={reset}>Retry</button>
227
+ * <a href="/products">Back to products</a>
203
228
  * </div>
204
229
  * )),
205
230
  * ])
@@ -317,6 +342,30 @@ const cache: RouteHelpers<any, any>["cache"] = (
317
342
  const name = `$${cacheIndex}`;
318
343
  const cacheConfig = { options };
319
344
 
345
+ // Among a path's children, cache() configures that path: the route entry
346
+ // carries the config, so buildEntriesAndCacheScope (router/route-snapshot.ts)
347
+ // makes the route the boundary and its own segments (handler, layouts,
348
+ // parallels) the cached unit. Both forms leave ctx.parent unchanged, so
349
+ // every sibling and wrapped item still attaches where it was. Issue #912: a
350
+ // cache entry here was never an ancestor of the route (no scope, no store
351
+ // write). The same holds inside a routeless layout()/middleware()/
352
+ // transition() wrapper in a path (issue #918): a route's record is
353
+ // all-or-nothing, so the path is the only thing such a cache() can cache.
354
+ const route = enclosingRoute(ctx.parent);
355
+ if (route && ctx.parent) {
356
+ route.cache = cacheConfig;
357
+ if (!children) return { name, type: "cache" } as CacheItem;
358
+ const uses = runAndValidateUseItems(
359
+ store,
360
+ `${ctx.namespace}.${cacheIndex}`,
361
+ ctx.parent,
362
+ children,
363
+ "cache",
364
+ "children",
365
+ );
366
+ return { name, type: "cache", uses } as CacheItem;
367
+ }
368
+
320
369
  // If no children, create an orphan cache entry (like orphan layouts)
321
370
  // This allows cache() to wrap subsequent siblings
322
371
  if (!children) {
@@ -491,7 +540,19 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
491
540
  "children",
492
541
  );
493
542
 
494
- if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
543
+ if (isOrphan(result)) {
544
+ // Its middleware runs for every route of the enclosing entry
545
+ // (collectRouteMiddleware walks orphans), so the wrapper scopes nothing;
546
+ // the flat form says what runs. Issue #918: the nested layout() was
547
+ // dropped at render.
548
+ invariant(
549
+ !result.some((item) => item?.type === "layout"),
550
+ `middleware(fn, () => [...]) with no routes inside cannot contain layout() [${namespace}]. ` +
551
+ "The middleware runs for every route of the enclosing path or layout either way; " +
552
+ "list them as siblings instead: middleware(fn), layout(...)",
553
+ );
554
+ attachOrphanSibling(ctx.parent, entry);
555
+ }
495
556
 
496
557
  return {
497
558
  name: namespace,
@@ -714,15 +775,12 @@ const intercept = (
714
775
  const prefixedRouteName =
715
776
  isLocal && namePrefix ? `${namePrefix}.${bareRouteName}` : bareRouteName;
716
777
 
717
- // Create intercept entry with its own loaders/revalidate/middleware/when
778
+ // Create intercept entry with its own loaders/middleware/when
718
779
  const entry: InterceptEntry = {
719
780
  slotName: slotName as `@${string}`,
720
781
  routeName: prefixedRouteName,
721
782
  handler,
722
783
  middleware: [],
723
- revalidate: [],
724
- errorBoundary: [],
725
- notFoundBoundary: [],
726
784
  loader: [],
727
785
  when: [], // Selector conditions for conditional interception
728
786
  };
@@ -735,25 +793,44 @@ const intercept = (
735
793
  entry.when.push(...selectors);
736
794
  }
737
795
 
738
- // Merge handler.use defaults with explicit use
796
+ // Merge handler.use defaults with explicit use. mountSite null: the merged
797
+ // items are validated once below, so both sources get the same message.
739
798
  const handlerUseFn = resolveHandlerUse(handler);
740
- const mergedUse = mergeHandlerUse(handlerUseFn, useFn, "intercept");
799
+ const mergedUse = mergeHandlerUse(handlerUseFn, useFn, null);
741
800
 
742
- // Run merged use callback to collect loaders, revalidate, middleware, etc.
801
+ // Run merged use callback to collect loaders, middleware, etc.
743
802
  if (mergedUse) {
744
803
  // Capture layout() calls into a temporary array
745
804
  const capturedLayouts: EntryData[] = [];
746
805
 
747
- // Temporary parent so middleware/loader/revalidate/when attach to the
748
- // intercept entry; the loading get/set accessor mirrors writes onto `entry`.
806
+ // Temporary parent so middleware/loader attach to the intercept entry;
807
+ // the loading get/set accessor mirrors writes onto `entry`. It is a
808
+ // shallow spread of the enclosing entry, so every field a rejected helper
809
+ // writes to points at a throwaway here: a helper called but not returned
810
+ // from use() never lands on the enclosing layout. cache() writes into
811
+ // `layout` (capturedLayouts), or into `cache` when the enclosing entry
812
+ // is a route.
813
+ //
814
+ // Why an intercept rejects these (validateInterceptUseItems):
815
+ // - revalidate(): an intercept only evaluates its loaders' revalidate()
816
+ // (router/intercept-resolution.ts).
817
+ // - errorBoundary()/notFoundBoundary(): error lookup walks EntryData
818
+ // parent chains (router/error-handling.ts), which never include an
819
+ // InterceptEntry; intercept loader errors resolve against the declaring
820
+ // entry's boundaries instead.
821
+ // - cache(): an intercept navigation is cached under the target route's
822
+ // cache() scope, with its own "intercept:" key (cache/cache-scope.ts).
749
823
  const tempParent = {
750
824
  ...ctx.parent,
751
825
  middleware: entry.middleware,
752
- revalidate: entry.revalidate,
753
- errorBoundary: entry.errorBoundary,
754
- notFoundBoundary: entry.notFoundBoundary,
755
826
  loader: entry.loader,
756
- layout: capturedLayouts, // Capture layout() calls
827
+ layout: capturedLayouts,
828
+ revalidate: [],
829
+ errorBoundary: [],
830
+ notFoundBoundary: [],
831
+ parallel: {},
832
+ intercept: [],
833
+ cache: undefined,
757
834
  get loading() {
758
835
  return entry.loading;
759
836
  },
@@ -762,19 +839,29 @@ const intercept = (
762
839
  },
763
840
  };
764
841
 
765
- const result = withParent(ctx, tempParent as EntryData, () =>
766
- mergedUse()?.flat(3),
842
+ const result = validateUseItems(
843
+ withParent(ctx, tempParent as EntryData, () => mergedUse()?.flat(3)),
844
+ namespace,
845
+ "intercept",
846
+ "use",
847
+ );
848
+ validateInterceptUseItems(
849
+ result,
850
+ capturedLayouts,
851
+ slotName,
852
+ routeName,
853
+ tempParent,
767
854
  );
768
855
 
769
- // Extract layout from captured layouts (use first one if multiple)
770
- // Layout inside intercept should always be ReactNode or Handler, not Record slots
771
- if (capturedLayouts.length > 0 && capturedLayouts[0].type === "layout") {
772
- entry.layout = capturedLayouts[0].handler as
773
- | ReactNode
774
- | Handler<any, any, any>;
856
+ // The modal chrome is the first returned layout() item. Match it by id so
857
+ // an entry pushed into capturedLayouts without being returned is never
858
+ // taken as the chrome. It carries no use() items (validated above).
859
+ const chromeItem = result.find((item) => item?.type === "layout");
860
+ const chrome =
861
+ chromeItem && capturedLayouts.find((l) => l.id === chromeItem.name);
862
+ if (chrome) {
863
+ entry.layout = chrome.handler as ReactNode | Handler<any, any, any>;
775
864
  }
776
-
777
- validateUseItems(result, namespace, "intercept", "use");
778
865
  }
779
866
 
780
867
  ctx.parent.intercept.push(entry);
@@ -822,12 +909,13 @@ const loader: RouteHelpers<any, any>["loader"] = (
822
909
  // resolved here, at DSL-evaluation time, for the same reason
823
910
  // loading({ ssr: false }) is (below) — per-isSSR entry caching makes the
824
911
  // flag request-mode-correct with no isSSR threading; see LoaderEntry.
912
+ // `bake` is the lane, the same on both evaluations.
825
913
  const loaderEntry: LoaderEntry = {
826
914
  loader: loaderDef,
827
915
  revalidate: [] as ShouldRevalidateFn<any, any>[],
828
- ...(optionsGiven?.ssr === false && ctx.isSSR
829
- ? { awaitBeforeFlush: true as const }
830
- : {}),
916
+ ...(optionsGiven?.ssr === false && { bake: true as const }),
917
+ ...(optionsGiven?.ssr === false &&
918
+ ctx.isSSR && { awaitBeforeFlush: true as const }),
831
919
  };
832
920
 
833
921
  // Merge handler.use defaults (attached to the loader definition) with explicit use
@@ -180,10 +180,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
180
180
  * intercept("@modal", "card", () => <CardModal />),
181
181
  * ])
182
182
  *
183
- * // With loaders and revalidation
183
+ * // With loaders and revalidation (revalidate() goes on the loader;
184
+ * // an intercept-level revalidate() throws)
184
185
  * intercept("@modal", "card", () => <CardModal />, () => [
185
- * loader(CardModalLoader),
186
- * revalidate(() => false),
186
+ * loader(CardModalLoader, () => [revalidate(() => false)]),
187
187
  * ])
188
188
  *
189
189
  * // Conditional activation via the config object's `when` selector
@@ -205,7 +205,11 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
205
205
  * @param handler - Component or handler for intercepted render
206
206
  * @param config - Optional InterceptConfig (e.g. `{ when }`), or the use
207
207
  * callback directly when there is no config
208
- * @param use - Optional callback for loaders, middleware, revalidate, etc.
208
+ * @param use - Optional callback for loader(), middleware(), loading(),
209
+ * transition() and a layout() modal chrome without use() items.
210
+ * revalidate() (goes on the loader), errorBoundary()/notFoundBoundary()
211
+ * (go on the enclosing layout) and cache() (goes on the target route)
212
+ * throw here.
209
213
  */
210
214
  intercept: {
211
215
  // Local: dot-prefixed, params inferred from local route definition
@@ -285,6 +289,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
285
289
  * ({ defaultShouldRevalidate: true })
286
290
  * )
287
291
  * ```
292
+ * Not valid directly inside intercept() use() (throws at definition time):
293
+ * intercepts only revalidate their loaders, so attach it there with
294
+ * `loader(Def, () => [revalidate(...)])`.
288
295
  * @param fn - Function returning either:
289
296
  * - `boolean` (hard decision — short-circuits the chain),
290
297
  * - `{ defaultShouldRevalidate: boolean }` (soft — updates the suggestion
@@ -352,16 +359,19 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
352
359
  * ```typescript
353
360
  * errorBoundary(<ErrorFallback />)
354
361
  *
355
- * // With dynamic error handler
356
- * errorBoundary(({ error, reset }) => (
362
+ * // With dynamic error handler (server fallback: receives only { error })
363
+ * errorBoundary(({ error }) => (
357
364
  * <div>
358
365
  * <h2>Something went wrong</h2>
359
366
  * <p>{error.message}</p>
360
- * <button onClick={reset}>Try again</button>
367
+ * <a href="/">Go home</a>
361
368
  * </div>
362
369
  * ))
363
370
  * ```
364
- * @param fallback - Static JSX or handler receiving error info and reset function
371
+ * Not valid inside intercept() use() (throws at definition time): an
372
+ * intercept has no boundary of its own, and its loader errors resolve
373
+ * against the enclosing layout/path boundary.
374
+ * @param fallback - Static JSX or handler receiving `{ error }`
365
375
  */
366
376
  errorBoundary: (
367
377
  fallback: ReactNode | ErrorBoundaryHandler,
@@ -379,6 +389,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
379
389
  * </div>
380
390
  * ))
381
391
  * ```
392
+ * Not valid inside intercept() use() (throws at definition time): an
393
+ * intercept has no boundary of its own, and notFound() from its loaders
394
+ * resolves against the enclosing layout/path boundary.
382
395
  * @param fallback - Static JSX or handler receiving not-found info
383
396
  */
384
397
  notFoundBoundary: (
@@ -396,6 +409,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
396
409
  * Note: Loaders are NOT cached by default. Use cache() inside loader()
397
410
  * to explicitly opt-in to loader caching.
398
411
  *
412
+ * Not valid inside intercept() use() (throws at definition time): an
413
+ * intercept navigation is cached under the target route's cache() scope.
414
+ *
399
415
  * ```typescript
400
416
  * // Using app-level defaults (ttl inherited from store.defaults)
401
417
  * cache(() => [
@@ -430,6 +446,12 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
430
446
  * route("archive/:year"), // uses KV store
431
447
  * ])
432
448
  *
449
+ * // Among a path's children: caches that path (handler, layouts, parallels)
450
+ * path("/product/:id", ProductPage, { name: "product" }, () => [
451
+ * cache({ ttl: 60 }),
452
+ * layout(<ProductChrome />), // cached with the path
453
+ * ])
454
+ *
433
455
  * // Opt-in loader caching
434
456
  * route("product/:id", ProductHandler, () => [
435
457
  * loader(ProductLoader), // NOT cached (default)
@@ -459,7 +481,9 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
459
481
  * content while the new loader resolves instead of flashing the route's
460
482
  * loading() skeleton (see segment-system.tsx inTransitionScope). Held
461
483
  * useLoader readers of the re-run loaders report isLoading: true until
462
- * the new data commits (use-loader.tsx). This is also the precondition
484
+ * the new data commits (use-loader.tsx). The pin is per loader family
485
+ * (`$$id`), not per segment — a layout reader of the same createLoader
486
+ * the child is re-running is flagged too. This is also the precondition
463
487
  * for any view-transition animation.
464
488
  * 2. <ViewTransition> (experimental React only): the segment content is also
465
489
  * wrapped in React's <ViewTransition>, so the held swap cross-fades/morphs.
@@ -91,13 +91,12 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
91
91
  "notFoundBoundary",
92
92
  "transition",
93
93
  ]),
94
+ // intercept() checks its whole merged use() (explicit and handler.use)
95
+ // against this set via validateInterceptUseItems below.
94
96
  intercept: new Set([
95
97
  "middleware",
96
- "revalidate",
97
98
  "loader",
98
99
  "loading",
99
- "errorBoundary",
100
- "notFoundBoundary",
101
100
  "layout",
102
101
  "route",
103
102
  "transition",
@@ -106,6 +105,120 @@ const MOUNT_SITE_ALLOWED_TYPES: Record<string, Set<string>> = {
106
105
  loader: new Set(["revalidate", "cache"]),
107
106
  };
108
107
 
108
+ const INTERCEPT_BOUNDARY_HINT =
109
+ "put it on the enclosing layout or path; it handles the intercept's handler and loader errors";
110
+
111
+ /**
112
+ * Where each item intercept() rejects goes instead. Why an intercept cannot
113
+ * hold them is documented at intercept() in dsl-helpers.ts.
114
+ */
115
+ const INTERCEPT_REJECTION_HINTS: Record<string, string> = {
116
+ revalidate:
117
+ "attach it to the intercept's loader: loader(YourLoader, () => [revalidate(...)])",
118
+ errorBoundary: INTERCEPT_BOUNDARY_HINT,
119
+ notFoundBoundary: INTERCEPT_BOUNDARY_HINT,
120
+ cache:
121
+ 'put cache() on the target route (intercept navigations get their own "intercept:" cache key) or use "use cache" in the handler',
122
+ };
123
+
124
+ function rejectInterceptItem(
125
+ what: string,
126
+ hint: string,
127
+ slotName: string,
128
+ routeName: string,
129
+ ): never {
130
+ throw new Error(
131
+ `${what} is not valid inside intercept("${slotName}", "${routeName}") use() (including the handler's .use): ${hint}.`,
132
+ );
133
+ }
134
+
135
+ /**
136
+ * Validate every item an intercept's merged use() returned (explicit use()
137
+ * and handler.use) against MOUNT_SITE_ALLOWED_TYPES.intercept, plus two
138
+ * structural rules:
139
+ * - a nested layout() is only the modal chrome (intercept() keeps its
140
+ * handler), so it may not carry use() items of its own;
141
+ * - a cache entry in `capturedLayouts` means cache() ran in the scope even if
142
+ * it was not returned; its orphan form re-parents the following siblings
143
+ * onto that entry, so it is rejected the same way. Inside a path, cache()
144
+ * writes `sinks.cache` instead (dsl-helpers.ts cache()).
145
+ */
146
+ export function validateInterceptUseItems(
147
+ items: AllUseItems[],
148
+ capturedLayouts: readonly { type: string }[],
149
+ slotName: string,
150
+ routeName: string,
151
+ sinks: {
152
+ readonly revalidate: readonly unknown[];
153
+ readonly errorBoundary: readonly unknown[];
154
+ readonly notFoundBoundary: readonly unknown[];
155
+ readonly intercept: readonly unknown[];
156
+ readonly parallel: Readonly<Record<string, unknown>>;
157
+ /** Set by cache() when the intercept is declared inside a path. */
158
+ readonly cache?: unknown;
159
+ },
160
+ ): void {
161
+ const allowed = MOUNT_SITE_ALLOWED_TYPES.intercept!;
162
+ for (const item of items) {
163
+ if (item == null) continue;
164
+ const type = (item as { type: string }).type;
165
+ if (!allowed.has(type)) {
166
+ rejectInterceptItem(
167
+ `${type}()`,
168
+ INTERCEPT_REJECTION_HINTS[type] ??
169
+ `allowed items are ${[...allowed].join(", ")}`,
170
+ slotName,
171
+ routeName,
172
+ );
173
+ }
174
+ if (type === "layout" && (item as { uses?: unknown[] }).uses?.length) {
175
+ rejectInterceptItem(
176
+ "layout() with its own use() items",
177
+ "put the modal chrome in the layout component and attach loaders to the intercept itself",
178
+ slotName,
179
+ routeName,
180
+ );
181
+ }
182
+ }
183
+ // A rejected helper called without being returned still wrote into its
184
+ // throwaway field on the temporary parent; reject it the same way.
185
+ for (const type of [
186
+ "revalidate",
187
+ "errorBoundary",
188
+ "notFoundBoundary",
189
+ "intercept",
190
+ ] as const) {
191
+ if (sinks[type].length > 0) {
192
+ rejectInterceptItem(
193
+ `${type}()`,
194
+ INTERCEPT_REJECTION_HINTS[type] ??
195
+ `allowed items are ${[...allowed].join(", ")}`,
196
+ slotName,
197
+ routeName,
198
+ );
199
+ }
200
+ }
201
+ if (Object.keys(sinks.parallel).length > 0) {
202
+ rejectInterceptItem(
203
+ "parallel()",
204
+ `allowed items are ${[...allowed].join(", ")}`,
205
+ slotName,
206
+ routeName,
207
+ );
208
+ }
209
+ if (
210
+ capturedLayouts.some((l) => l.type === "cache") ||
211
+ sinks.cache !== undefined
212
+ ) {
213
+ rejectInterceptItem(
214
+ "cache()",
215
+ INTERCEPT_REJECTION_HINTS.cache!,
216
+ slotName,
217
+ routeName,
218
+ );
219
+ }
220
+ }
221
+
109
222
  /**
110
223
  * Validate that items from handler.use() are valid for the given mount site.
111
224
  * Throws a descriptive error if any item is not allowed.
@@ -135,7 +248,7 @@ export function validateHandlerUseItems(
135
248
  export function mergeHandlerUse(
136
249
  handlerUse: (() => any[]) | undefined,
137
250
  explicitUse: (() => any[]) | undefined,
138
- mountSite: string,
251
+ mountSite: string | null,
139
252
  ): (() => any[]) | undefined {
140
253
  if (!handlerUse && !explicitUse) return undefined;
141
254
  // Validation asymmetry (intentional, pre-1.0): only handler.use() items are
@@ -144,17 +257,19 @@ export function mergeHandlerUse(
144
257
  // branch here and the merged branch, so a structurally-valid-but-prohibited
145
258
  // item (e.g. middleware() inside a parallel slot) is not rejected at this seam.
146
259
  // Documented rather than enforced for now; revisit before 1.0 (#569).
260
+ // intercept() is the exception: it passes mountSite null and validates the
261
+ // merged items itself (validateInterceptUseItems).
147
262
  if (!handlerUse) return explicitUse;
148
263
  if (!explicitUse) {
149
264
  return () => {
150
265
  const items = handlerUse().flat(3);
151
- validateHandlerUseItems(items, mountSite);
266
+ if (mountSite) validateHandlerUseItems(items, mountSite);
152
267
  return items;
153
268
  };
154
269
  }
155
270
  return () => {
156
271
  const hItems = handlerUse().flat(3);
157
- validateHandlerUseItems(hItems, mountSite);
272
+ if (mountSite) validateHandlerUseItems(hItems, mountSite);
158
273
  return [...hItems, ...explicitUse()];
159
274
  };
160
275
  }
@@ -222,11 +222,8 @@ export type ParallelUseItem =
222
222
  | TransitionItem;
223
223
  export type InterceptUseItem =
224
224
  | MiddlewareItem
225
- | RevalidateItem
226
225
  | LoaderItem
227
226
  | LoadingItem
228
- | ErrorBoundaryItem
229
- | NotFoundBoundaryItem
230
227
  | LayoutItem
231
228
  | RouteItem
232
229
  | TransitionItem;
@@ -5,8 +5,9 @@ import MapRootLayout from "../server/root-layout.js";
5
5
  import type { RouteEntry, TrailingSlashMode } from "../types";
6
6
 
7
7
  /**
8
- * Build a serialized manifest from all route entries for debug inspection.
9
- * Used by the `debugManifest()` method on the router instance.
8
+ * Build a serialized manifest from the eager route entries for debug
9
+ * inspection. Used by the `debugManifest()` method on the router instance.
10
+ * include() children are not expanded (lazy placeholders are skipped).
10
11
  */
11
12
  export async function buildDebugManifest<TEnv = any>(
12
13
  routesEntries: RouteEntry<TEnv>[],
@@ -14,6 +15,13 @@ export async function buildDebugManifest<TEnv = any>(
14
15
  const manifest = new Map<string, EntryData>();
15
16
 
16
17
  for (const entry of routesEntries) {
18
+ // Lazy include placeholders are not mounts: a top-level one carries the
19
+ // parent urls() handler (router.ts routes()), so re-running it re-registers
20
+ // the parent's routes into the shared manifest (path() duplicate-name
21
+ // invariant). A nested one (lazy-includes.ts runExpansion()) is only valid
22
+ // under its captured include context.
23
+ if (entry.lazy) continue;
24
+
17
25
  const Store = {
18
26
  manifest,
19
27
  namespace: `debug.M${entry.mountIndex}`,