@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad

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 (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -11,12 +11,17 @@ import {
11
11
  getContext,
12
12
  getNamePrefix,
13
13
  getUrlPrefix,
14
+ requireDslContext,
14
15
  type EntryData,
16
+ type EntryPropDatas,
17
+ type EntryPropSegments,
18
+ type HelperContext,
15
19
  type InterceptEntry,
16
20
  } from "../server/context";
17
21
  import { invariant } from "../errors";
22
+ import { validateUserRouteName } from "../route-name.js";
18
23
  import { isCachedFunction } from "../cache/taint.js";
19
- import { RSCRouterContext } from "../server/context";
24
+ import { RangoContext } from "../server/context";
20
25
  import { isStaticHandler } from "../static-handler.js";
21
26
  import RootLayout from "../server/root-layout";
22
27
  import type {
@@ -37,6 +42,8 @@ import type {
37
42
  UseItems,
38
43
  } from "../route-types.js";
39
44
  import type { RouteHelpers } from "./helpers-types.js";
45
+ import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
46
+ import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
40
47
 
41
48
  /**
42
49
  * Check if an item contains routes (directly or inside nested structures like cache).
@@ -54,19 +61,111 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
54
61
  if (item.type === "layout" && item.uses) {
55
62
  return item.uses.some((child) => hasRoutesInItem(child));
56
63
  }
64
+ if (item.type === "middleware" && item.uses) {
65
+ return item.uses.some((child) => hasRoutesInItem(child));
66
+ }
57
67
  return false;
58
68
  };
59
69
 
70
+ /**
71
+ * Fresh empty collections shared by every from-scratch segment entry. Returns
72
+ * new arrays/objects per call so no two entries share mutable references.
73
+ * mountPath is intentionally NOT included here — each call site adds it from
74
+ * getUrlPrefix() where applicable: the route() and transition() helpers add
75
+ * none, while path() (which also builds a `type: "route"` entry) and the
76
+ * structural helpers (layout/cache/middleware/parallel) do.
77
+ */
78
+ const emptySegmentBase = (): EntryPropDatas &
79
+ EntryPropSegments & { loading: undefined } => ({
80
+ loading: undefined,
81
+ middleware: [],
82
+ revalidate: [],
83
+ errorBoundary: [],
84
+ notFoundBoundary: [],
85
+ layout: [],
86
+ parallel: {},
87
+ intercept: [],
88
+ loader: [],
89
+ });
90
+
91
+ /**
92
+ * Run a children/use callback as a nested scope, flatten the result, and assert
93
+ * every item is a valid use item. `kind` preserves the existing error wording
94
+ * ("use()" vs "children" callback).
95
+ */
96
+ function runAndValidateUseItems(
97
+ store: ReturnType<typeof getContext>,
98
+ namespace: string,
99
+ entry: EntryData,
100
+ cb: () => any,
101
+ label: string,
102
+ kind: "use" | "children",
103
+ ): AllUseItems[] {
104
+ const result = store.run(namespace, entry, cb)?.flat(3);
105
+ return validateUseItems(result, namespace, label, kind);
106
+ }
107
+
108
+ /** Assert an already-invoked, flattened callback result is a use-item array. */
109
+ function validateUseItems(
110
+ result: any,
111
+ namespace: string,
112
+ label: string,
113
+ kind: "use" | "children",
114
+ ): AllUseItems[] {
115
+ invariant(
116
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
117
+ `${label}() ${kind === "use" ? "use()" : "children"} callback must return an array of use items [${namespace}]`,
118
+ );
119
+ return result as AllUseItems[];
120
+ }
121
+
122
+ /** True when a children/use result contains no routes (directly or nested). */
123
+ const isOrphan = (result: AllUseItems[]): boolean =>
124
+ !result.some((item) => item != null && hasRoutesInItem(item));
125
+
126
+ /**
127
+ * Register a routeless structural entry as an orphan sibling: clear its parent
128
+ * pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
129
+ * docs/tree-structure.md) and push it onto the parent's layout[] so it renders
130
+ * as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
131
+ * validation and registers inline.
132
+ */
133
+ const attachOrphanSibling = (
134
+ parent: EntryData | null,
135
+ entry: EntryData,
136
+ ): void => {
137
+ entry.parent = null;
138
+ if (parent && "layout" in parent) parent.layout.push(entry);
139
+ };
140
+
141
+ /**
142
+ * Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
143
+ * entry that captures the attachments declared by a use() callback — restoring
144
+ * the original parent afterward, including on throw. loader()/intercept() each
145
+ * build their own tempParent shape (intercept keeps a loading get/set accessor
146
+ * and a captured-layouts array); this only centralizes the save/restore.
147
+ */
148
+ function withParent<T>(ctx: HelperContext, temp: EntryData, fn: () => T): T {
149
+ const original = ctx.parent;
150
+ ctx.parent = temp;
151
+ try {
152
+ return fn();
153
+ } finally {
154
+ ctx.parent = original;
155
+ }
156
+ }
157
+
60
158
  const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
61
- const ctx = getContext().getStore();
62
- if (!ctx) throw new Error("revalidate() must be called inside map()");
159
+ const { store, ctx } = requireDslContext(
160
+ "revalidate() must be called inside urls()",
161
+ );
63
162
 
64
163
  // Attach to last entry in stack
65
164
  const parent = ctx.parent;
66
165
  if (!parent || !("revalidate" in parent)) {
67
166
  invariant(false, "No parent entry available for revalidate()");
68
167
  }
69
- const name = `$${getContext().getNextIndex("revalidate")}`;
168
+ const name = `$${store.getNextIndex("revalidate")}`;
70
169
  parent.revalidate.push(fn);
71
170
  return { name, type: "revalidate" } as RevalidateItem;
72
171
  };
@@ -104,15 +203,16 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
104
203
  * ```
105
204
  */
106
205
  const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
107
- const ctx = getContext().getStore();
108
- if (!ctx) throw new Error("errorBoundary() must be called inside map()");
206
+ const { store, ctx } = requireDslContext(
207
+ "errorBoundary() must be called inside urls()",
208
+ );
109
209
 
110
210
  // Attach to parent entry in stack
111
211
  const parent = ctx.parent;
112
212
  if (!parent || !("errorBoundary" in parent)) {
113
213
  invariant(false, "No parent entry available for errorBoundary()");
114
214
  }
115
- const name = `$${getContext().getNextIndex("errorBoundary")}`;
215
+ const name = `$${store.getNextIndex("errorBoundary")}`;
116
216
  parent.errorBoundary.push(fallback);
117
217
  return { name, type: "errorBoundary" } as ErrorBoundaryItem;
118
218
  };
@@ -151,15 +251,16 @@ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
151
251
  const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
152
252
  fallback,
153
253
  ) => {
154
- const ctx = getContext().getStore();
155
- if (!ctx) throw new Error("notFoundBoundary() must be called inside map()");
254
+ const { store, ctx } = requireDslContext(
255
+ "notFoundBoundary() must be called inside urls()",
256
+ );
156
257
 
157
258
  // Attach to parent entry in stack
158
259
  const parent = ctx.parent;
159
260
  if (!parent || !("notFoundBoundary" in parent)) {
160
261
  invariant(false, "No parent entry available for notFoundBoundary()");
161
262
  }
162
- const name = `$${getContext().getNextIndex("notFoundBoundary")}`;
263
+ const name = `$${store.getNextIndex("notFoundBoundary")}`;
163
264
  parent.notFoundBoundary.push(fallback);
164
265
  return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
165
266
  };
@@ -173,8 +274,9 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
173
274
  * for the intercept to activate.
174
275
  */
175
276
  const when: RouteHelpers<any, any>["when"] = (fn) => {
176
- const ctx = getContext().getStore();
177
- if (!ctx) throw new Error("when() must be called inside intercept()");
277
+ const { store, ctx } = requireDslContext(
278
+ "when() must be called inside intercept()",
279
+ );
178
280
 
179
281
  // The when() function needs to be captured by the intercept's tempParent
180
282
  // which should have a `when` array. If not present, we're not inside intercept()
@@ -186,7 +288,7 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
186
288
  );
187
289
  }
188
290
 
189
- const name = `$${getContext().getNextIndex("when")}`;
291
+ const name = `$${store.getNextIndex("when")}`;
190
292
  parent.when.push(fn);
191
293
  return { name, type: "when" } as WhenItem;
192
294
  };
@@ -201,21 +303,21 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
201
303
  * Supports these call signatures:
202
304
  * - cache() - no args, uses app-level defaults (for loader caching)
203
305
  * - cache(() => [...]) - wraps children with app-level defaults
204
- * - cache('profileName') - uses a named cache profile
205
- * - cache('profileName', () => [...]) - named profile with children
206
306
  * - cache({ ttl: 60 }, () => [...]) - with explicit options
307
+ *
308
+ * Named cache profiles are applied via the `"use cache: <profile>"` directive,
309
+ * not a `cache("profileName")` form in the route tree.
207
310
  */
208
311
  const cache: RouteHelpers<any, any>["cache"] = (
209
312
  optionsOrChildren?:
210
313
  | PartialCacheOptions
211
314
  | false
212
- | string
213
315
  | (() => UseItems<AllUseItems>),
214
316
  maybeChildren?: () => UseItems<AllUseItems>,
215
317
  ) => {
216
- const store = getContext();
217
- const ctx = store.getStore();
218
- if (!ctx) throw new Error("cache() must be called inside map()");
318
+ const { store, ctx } = requireDslContext(
319
+ "cache() must be called inside urls()",
320
+ );
219
321
 
220
322
  // Handle overloaded signature
221
323
  let options: PartialCacheOptions | false;
@@ -225,18 +327,6 @@ const cache: RouteHelpers<any, any>["cache"] = (
225
327
  // cache() - no args, use defaults
226
328
  options = {};
227
329
  children = undefined;
228
- } else if (typeof optionsOrChildren === "string") {
229
- // cache('profileName') or cache('profileName', () => [...])
230
- // Resolve from context-scoped profiles (set per-router via HelperContext).
231
- const ctxStore = RSCRouterContext.getStore();
232
- const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
233
- invariant(
234
- profile,
235
- `cache("${optionsOrChildren}"): unknown cache profile. ` +
236
- `Define it in createRouter({ cacheProfiles: { "${optionsOrChildren}": { ttl: ... } } }).`,
237
- );
238
- options = { ttl: profile.ttl, swr: profile.swr, tags: profile.tags };
239
- children = maybeChildren;
240
330
  } else if (typeof optionsOrChildren === "function") {
241
331
  // cache(() => [...]) - use empty options (will use defaults)
242
332
  options = {};
@@ -267,26 +357,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
267
357
  // Create orphan cache entry (like orphan layout)
268
358
  // Subsequent siblings in the same array will attach to this entry
269
359
  const namespace = `${ctx.namespace}.${cacheIndex}`;
270
- const cacheUrlPrefix = getUrlPrefix();
360
+ const urlPrefix = getUrlPrefix();
271
361
 
272
362
  const entry = {
363
+ ...emptySegmentBase(),
273
364
  id: namespace,
274
365
  shortCode: store.getShortCode("cache"),
275
366
  type: "cache",
276
367
  parent: parent, // link to current parent for hierarchy
277
368
  cache: cacheConfig,
278
369
  handler: RootLayout,
279
- loading: undefined, // Allow loading() to attach loading state
280
- middleware: [],
281
- revalidate: [],
282
- errorBoundary: [],
283
- notFoundBoundary: [],
284
- layout: [],
285
- parallel: [],
286
- intercept: [],
287
- loader: [],
288
- ...(cacheUrlPrefix ? { mountPath: cacheUrlPrefix } : {}),
289
- } as EntryData;
370
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
371
+ } satisfies EntryData;
290
372
 
291
373
  // Attach to parent's layout array (cache entries are structural like layouts)
292
374
  if (parent && "layout" in parent) {
@@ -300,13 +382,23 @@ const cache: RouteHelpers<any, any>["cache"] = (
300
382
  return { name: namespace, type: "cache" } as CacheItem;
301
383
  }
302
384
 
385
+ // Inside a loader() use() callback, only the direct form — cache()/cache(opts)
386
+ // — writes cache config to the loader entry. The wrapper form creates a
387
+ // structural cache boundary with its own children scope, which has no effect
388
+ // on the loader and would silently no-op.
389
+ invariant(
390
+ !(ctx.parent && (ctx.parent as any).type === "loader"),
391
+ "cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
392
+ );
393
+
303
394
  // With children: create a cache entry (like layout with caching semantics)
304
395
  const namespace = `${ctx.namespace}.${cacheIndex}`;
305
396
  const cacheShortCode = store.getShortCode("cache");
306
397
 
307
- const cacheUrlPrefix2 = getUrlPrefix();
398
+ const urlPrefix = getUrlPrefix();
308
399
 
309
400
  const entry = {
401
+ ...emptySegmentBase(),
310
402
  id: namespace,
311
403
  shortCode: cacheShortCode,
312
404
  type: "cache",
@@ -314,48 +406,57 @@ const cache: RouteHelpers<any, any>["cache"] = (
314
406
  cache: cacheConfig,
315
407
  // Cache entries render like layouts (with Outlet as default handler)
316
408
  handler: RootLayout, // RootLayout just renders <Outlet />
317
- loading: undefined, // Allow loading() to attach loading state
318
- middleware: [],
319
- revalidate: [],
320
- errorBoundary: [],
321
- notFoundBoundary: [],
322
- layout: [],
323
- parallel: [],
324
- intercept: [],
325
- loader: [],
326
- ...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
327
- } as EntryData;
409
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
410
+ } satisfies EntryData;
328
411
 
329
412
  // Run children with cache entry as parent
330
- const result = store.run(namespace, entry, children)?.flat(3);
331
-
332
- invariant(
333
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
334
- `cache() children callback must return an array of use items [${namespace}]`,
413
+ const result = runAndValidateUseItems(
414
+ store,
415
+ namespace,
416
+ entry,
417
+ children,
418
+ "cache",
419
+ "children",
335
420
  );
336
421
 
337
- // Check if this cache has routes (including nested caches/layouts)
338
- const hasRoutes =
339
- result &&
340
- Array.isArray(result) &&
341
- result.some((item) => hasRoutesInItem(item));
342
-
343
- if (!hasRoutes) {
344
- const parent = ctx.parent;
345
- if (parent && "layout" in parent) {
346
- // Attach to parent's layout array (cache entries are structural like layouts)
347
- entry.parent = null;
348
- parent.layout.push(entry);
349
- }
350
- }
422
+ // Cache entries are structural like layouts: with no routes inside, register
423
+ // as an orphan sibling.
424
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
351
425
 
352
426
  return { name: namespace, type: "cache", uses: result } as CacheItem;
353
427
  };
354
428
 
355
- const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
429
+ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
430
+ // Four call forms:
431
+ // middleware(fn) — single fn, sibling
432
+ // middleware(fn, () => [...]) — single fn, wrapping
433
+ // middleware([fn1, fn2]) — array, sibling
434
+ // middleware([fn1, fn2], () => [...]) — array, wrapping
435
+ const isArray = Array.isArray(args[0]);
436
+
437
+ // Reject the removed variadic form before executing anything.
438
+ // middleware(fn1, fn2, fn3) — 3+ args, always wrong.
439
+ // middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
440
+ // children callback (length === 0) — legacy two-fn form, reject early.
441
+ if (
442
+ args.length > 2 ||
443
+ (!isArray &&
444
+ args.length === 2 &&
445
+ typeof args[1] === "function" &&
446
+ args[1].length > 0)
447
+ ) {
448
+ throw new Error(
449
+ "middleware() no longer accepts variadic arguments. " +
450
+ "Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
451
+ );
452
+ }
453
+
454
+ const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
455
+ const children: (() => any[]) | undefined =
456
+ typeof args[1] === "function" ? args[1] : undefined;
457
+
356
458
  // Prevent "use cache" functions from being used as middleware.
357
- // Checked before context validation — this is a static invariant.
358
- for (const f of fn) {
459
+ for (const f of fns) {
359
460
  if (isCachedFunction(f)) {
360
461
  throw new Error(
361
462
  `A "use cache" function cannot be used as middleware. ` +
@@ -366,23 +467,84 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...fn) => {
366
467
  }
367
468
  }
368
469
 
369
- const ctx = getContext().getStore();
370
- if (!ctx) throw new Error("middleware() must be called inside map()");
470
+ const { store, ctx } = requireDslContext(
471
+ "middleware() must be called inside urls()",
472
+ );
371
473
 
372
- // Attach to last entry in stack
373
- const parent = ctx.parent;
374
- if (!parent || !("middleware" in parent)) {
375
- invariant(false, "No parent entry available for middleware()");
474
+ if (!children) {
475
+ // Sibling mode: attach to parent entry
476
+ const parent = ctx.parent;
477
+ if (!parent || !("middleware" in parent)) {
478
+ invariant(false, "No parent entry available for middleware()");
479
+ }
480
+ const name = `$${store.getNextIndex("middleware")}`;
481
+ parent.middleware.push(...fns);
482
+ return { name, type: "middleware" } as MiddlewareItem;
376
483
  }
377
- const name = `$${getContext().getNextIndex("middleware")}`;
378
- parent.middleware.push(...fn);
379
- return { name, type: "middleware" } as MiddlewareItem;
484
+
485
+ // Wrapping mode: create a transparent layout that carries the middleware
486
+ const mwIndex = store.getNextIndex("middleware");
487
+ const namespace = `${ctx.namespace}.${mwIndex}`;
488
+
489
+ const urlPrefix = getUrlPrefix();
490
+ const entry = {
491
+ ...emptySegmentBase(),
492
+ id: namespace,
493
+ shortCode: store.getShortCode("layout"),
494
+ type: "layout",
495
+ parent: ctx.parent,
496
+ handler: RootLayout,
497
+ middleware: [...fns],
498
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
499
+ } satisfies EntryData;
500
+
501
+ // Run children callback. If the second arg was actually a middleware fn
502
+ // (old variadic form: middleware(mw1, mw2)), this will return a non-array
503
+ // and the invariant below gives a clear migration error.
504
+ const rawResult = store.run(namespace, entry, children);
505
+
506
+ invariant(
507
+ Array.isArray(rawResult),
508
+ "middleware(fn, children) expects the second argument to return an array of use items. " +
509
+ "To pass multiple middleware, use middleware([fn1, fn2]).",
510
+ );
511
+
512
+ const result = validateUseItems(
513
+ rawResult.flat(3),
514
+ namespace,
515
+ "middleware",
516
+ "children",
517
+ );
518
+
519
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
520
+
521
+ return {
522
+ name: namespace,
523
+ type: "middleware",
524
+ uses: result,
525
+ } as MiddlewareItem;
380
526
  };
381
527
 
528
+ // Slot names become part of segment ids: a parallel/intercept slot is encoded
529
+ // as `${shortCode}.${slotName}`, and loader segments append `D${index}.${loaderId}`.
530
+ // A "." in the slot name collides with that separator -- loaderParentId
531
+ // (segment-system.tsx) strips from the FIRST `D<index>.`, so a name like
532
+ // "@D3.foo" is mis-cut to "@" and the loader's data is silently dropped. Reject
533
+ // the dot at definition time so the failure is loud, not a corrupted tree at
534
+ // runtime. (A bare "D" without a trailing dot -- e.g. "@Detail" -- is fine.)
535
+ function assertValidSlotName(slotName: string): void {
536
+ invariant(
537
+ !slotName.includes("."),
538
+ `Slot name "${slotName}" must not contain ".". The dot is a reserved ` +
539
+ `segment-id separator; a name like "@D3.foo" corrupts loader segment-id ` +
540
+ `parsing and silently drops the loader's data. Rename the slot.`,
541
+ );
542
+ }
543
+
382
544
  const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
383
- const store = getContext();
384
- const ctx = store.getStore();
385
- if (!ctx) throw new Error("parallel() must be called inside map()");
545
+ const { store, ctx } = requireDslContext(
546
+ "parallel() must be called inside urls()",
547
+ );
386
548
 
387
549
  if (!ctx.parent || !ctx.parent?.parallel) {
388
550
  invariant(false, "No parent entry available for parallel()");
@@ -393,15 +555,30 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
393
555
  "parallel() cannot be nested inside another parallel()",
394
556
  );
395
557
 
558
+ const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
559
+ for (const slotName of slotNames) assertValidSlotName(slotName);
560
+
396
561
  const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
397
562
 
398
- // Unwrap any static handler definitions in parallel slots
563
+ // Unwrap slot values. A slot value can be:
564
+ // - a Handler / ReactNode (legacy form)
565
+ // - a Static() definition (build-time only)
566
+ // - a slot descriptor `{ handler, use? }` for slot-local overrides
567
+ // The descriptor's `use` runs after the broadcast `use` for that slot,
568
+ // so single-assignment items like `loading()` placed there win without
569
+ // affecting siblings.
399
570
  const unwrappedSlots: Record<string, any> = {};
571
+ const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
400
572
  let hasStaticSlot = false;
401
573
  const staticSlotIds: Record<string, string> = {};
402
- for (const [slotName, slotHandler] of Object.entries(
574
+ for (const [slotName, rawSlot] of Object.entries(
403
575
  slots as Record<string, any>,
404
576
  )) {
577
+ let slotHandler: any = rawSlot;
578
+ if (isSlotDescriptor(rawSlot)) {
579
+ slotHandler = rawSlot.handler;
580
+ slotLocalUses[slotName] = rawSlot.use;
581
+ }
405
582
  if (isStaticHandler(slotHandler)) {
406
583
  hasStaticSlot = true;
407
584
  unwrappedSlots[slotName] = slotHandler.handler;
@@ -420,20 +597,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
420
597
  // Create full EntryData for parallel with its own loaders/revalidate/loading
421
598
  const parallelUrlPrefix = getUrlPrefix();
422
599
  const entry = {
600
+ ...emptySegmentBase(),
423
601
  id: namespace,
424
602
  shortCode: store.getShortCode("parallel"),
425
603
  type: "parallel",
426
604
  parent: null, // Parallels don't participate in parent chain traversal
427
605
  handler: unwrappedSlots,
428
- loading: undefined, // Allow loading() to attach loading state
429
- middleware: [],
430
- revalidate: [],
431
- errorBoundary: [],
432
- notFoundBoundary: [],
433
- layout: [],
434
- parallel: [],
435
- intercept: [],
436
- loader: [],
437
606
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
438
607
  ...(hasStaticSlot
439
608
  ? {
@@ -445,19 +614,86 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
445
614
  : {}),
446
615
  } satisfies EntryData;
447
616
 
448
- // Run use callback if provided to collect loaders, revalidate, loading
449
- if (use && typeof use === "function") {
450
- const result = store.run(namespace, entry, use)?.flat(3);
451
- invariant(
452
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
453
- `parallel() use() callback must return an array of use items [${namespace}]`,
617
+ for (const slotName of slotNames) {
618
+ const slotEntry = {
619
+ ...entry,
620
+ handler: { [slotName]: unwrappedSlots[slotName]! },
621
+ middleware: [...entry.middleware],
622
+ revalidate: [...entry.revalidate],
623
+ errorBoundary: [...entry.errorBoundary],
624
+ notFoundBoundary: [...entry.notFoundBoundary],
625
+ layout: [...entry.layout],
626
+ parallel: { ...entry.parallel },
627
+ intercept: [...entry.intercept],
628
+ loader: [...entry.loader],
629
+ ...(entry.staticHandlerIds?.[slotName]
630
+ ? {
631
+ isStaticPrerender: true as const,
632
+ staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
633
+ }
634
+ : {
635
+ isStaticPrerender: undefined,
636
+ staticHandlerIds: undefined,
637
+ }),
638
+ } satisfies EntryData;
639
+
640
+ // Per-slot merge order (narrowest-scope-wins for single-assignment items
641
+ // like loading()):
642
+ // 1. handler.use — defaults baked into the handler
643
+ // 2. shared `use` — broadcast at the parallel() call site
644
+ // 3. slot-local `use` — per-slot override via `{ handler, use }` descriptor
645
+ // Items that accumulate (loader, middleware, revalidate, …) compose
646
+ // across all three layers regardless of order.
647
+ const rawSlot = (slots as Record<string, any>)[slotName];
648
+ const slotHandlerForUse = isSlotDescriptor(rawSlot)
649
+ ? rawSlot.handler
650
+ : rawSlot;
651
+ const slotHandlerUse = resolveHandlerUse(slotHandlerForUse);
652
+ const slotLocalUse = slotLocalUses[slotName];
653
+ const explicitUse = combineExplicitUses(use, slotLocalUse);
654
+ const slotMergedUse = mergeHandlerUse(
655
+ slotHandlerUse,
656
+ explicitUse,
657
+ "parallel",
454
658
  );
455
- }
659
+ if (slotMergedUse) {
660
+ runAndValidateUseItems(
661
+ store,
662
+ namespace,
663
+ slotEntry,
664
+ slotMergedUse,
665
+ "parallel",
666
+ "use",
667
+ );
668
+ }
456
669
 
457
- ctx.parent.parallel.push(entry);
670
+ ctx.parent.parallel[slotName] = slotEntry;
671
+ }
458
672
  return { name: namespace, type: "parallel" } as ParallelItem;
459
673
  };
460
674
 
675
+ function isSlotDescriptor(
676
+ value: unknown,
677
+ ): value is { handler: unknown; use?: () => any[] } {
678
+ return (
679
+ typeof value === "object" &&
680
+ value !== null &&
681
+ !("__brand" in value) &&
682
+ "handler" in value &&
683
+ typeof (value as any).handler !== "undefined"
684
+ );
685
+ }
686
+
687
+ function combineExplicitUses(
688
+ sharedUse: (() => any[]) | undefined,
689
+ slotLocalUse: (() => any[]) | undefined,
690
+ ): (() => any[]) | undefined {
691
+ if (!sharedUse && !slotLocalUse) return undefined;
692
+ if (!slotLocalUse) return sharedUse;
693
+ if (!sharedUse) return slotLocalUse;
694
+ return () => [...sharedUse(), ...slotLocalUse()];
695
+ }
696
+
461
697
  /**
462
698
  * Intercept helper - defines an intercepting route for soft navigation
463
699
  */
@@ -467,9 +703,9 @@ const intercept = (
467
703
  handler: any,
468
704
  use?: () => any[],
469
705
  ) => {
470
- const store = getContext();
471
- const ctx = store.getStore();
472
- if (!ctx) throw new Error("intercept() must be called inside map()");
706
+ const { store, ctx } = requireDslContext(
707
+ "intercept() must be called inside urls()",
708
+ );
473
709
 
474
710
  if (!ctx.parent || !ctx.parent?.intercept) {
475
711
  invariant(false, "No parent entry available for intercept()");
@@ -480,6 +716,8 @@ const intercept = (
480
716
  "intercept() cannot be used inside parallel()",
481
717
  );
482
718
 
719
+ assertValidSlotName(slotName);
720
+
483
721
  const namespace = `${ctx.namespace}.$${store.getNextIndex("intercept")}.${slotName}`;
484
722
 
485
723
  // Dot-prefixed = local (add include prefix), unprefixed = global (use as-is)
@@ -502,17 +740,19 @@ const intercept = (
502
740
  when: [], // Selector conditions for conditional interception
503
741
  };
504
742
 
505
- // Run use callback if provided to collect loaders, revalidate, middleware, etc.
506
- if (use && typeof use === "function") {
507
- // Create a temporary parent context for the use() callback
508
- // so that middleware, loader, revalidate attach to the intercept entry
509
- const originalParent = ctx.parent;
743
+ // Merge handler.use defaults with explicit use
744
+ const handlerUseFn = resolveHandlerUse(handler);
745
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
510
746
 
511
- // Capture layouts in a temporary array
747
+ // Run merged use callback to collect loaders, revalidate, middleware, etc.
748
+ if (mergedUse) {
749
+ // Capture layout() calls into a temporary array
512
750
  const capturedLayouts: EntryData[] = [];
513
751
 
752
+ // Temporary parent so middleware/loader/revalidate/when attach to the
753
+ // intercept entry; the loading get/set accessor mirrors writes onto `entry`.
514
754
  const tempParent = {
515
- ...originalParent,
755
+ ...ctx.parent,
516
756
  middleware: entry.middleware,
517
757
  revalidate: entry.revalidate,
518
758
  errorBoundary: entry.errorBoundary,
@@ -520,7 +760,6 @@ const intercept = (
520
760
  loader: entry.loader,
521
761
  layout: capturedLayouts, // Capture layout() calls
522
762
  when: entry.when, // Capture when() conditions
523
- // Use getter/setter to capture loading on the entry
524
763
  get loading() {
525
764
  return entry.loading;
526
765
  },
@@ -528,12 +767,10 @@ const intercept = (
528
767
  entry.loading = value;
529
768
  },
530
769
  };
531
- ctx.parent = tempParent as EntryData;
532
770
 
533
- const result = use()?.flat(3);
534
-
535
- // Restore original parent
536
- ctx.parent = originalParent;
771
+ const result = withParent(ctx, tempParent as EntryData, () =>
772
+ mergedUse()?.flat(3),
773
+ );
537
774
 
538
775
  // Extract layout from captured layouts (use first one if multiple)
539
776
  // Layout inside intercept should always be ReactNode or Handler, not Record slots
@@ -543,10 +780,7 @@ const intercept = (
543
780
  | Handler<any, any, any>;
544
781
  }
545
782
 
546
- invariant(
547
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
548
- `intercept() use() callback must return an array of use items [${namespace}]`,
549
- );
783
+ validateUseItems(result, namespace, "intercept", "use");
550
784
  }
551
785
 
552
786
  ctx.parent.intercept.push(entry);
@@ -556,10 +790,10 @@ const intercept = (
556
790
  /**
557
791
  * Loader helper - attaches a loader to the current entry
558
792
  */
559
- const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
560
- const store = getContext();
561
- const ctx = store.getStore();
562
- if (!ctx) throw new Error("loader() must be called inside map()");
793
+ const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
794
+ const { store, ctx } = requireDslContext(
795
+ "loader() must be called inside urls()",
796
+ );
563
797
 
564
798
  // Attach to last entry in stack
565
799
  if (!ctx.parent || !ctx.parent?.loader) {
@@ -574,25 +808,28 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
574
808
  revalidate: [] as ShouldRevalidateFn<any, any>[],
575
809
  };
576
810
 
577
- // If use() callback provided, run it to collect revalidation rules and cache config
578
- if (use && typeof use === "function") {
579
- // Temporarily set context for revalidate()/cache() calls to target this loader
580
- const originalParent = ctx.parent;
811
+ // Merge handler.use defaults (attached to the loader definition) with explicit use
812
+ const handlerUseFn = resolveHandlerUse(loaderDef);
813
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "loader");
814
+
815
+ // If any use callback is in effect, run it to collect revalidation rules and cache config
816
+ if (mergedUse) {
581
817
  // Create a temporary "parent" with type "loader" so cache() can detect it.
582
818
  // Save existing .cache to distinguish inherited config from newly set config.
583
- const parentCache = (originalParent as any).cache;
819
+ const parentCache = (ctx.parent as any).cache;
584
820
  const tempParent = {
585
- ...originalParent,
821
+ ...ctx.parent,
586
822
  type: "loader",
587
823
  revalidate: loaderEntry.revalidate,
588
824
  };
589
- ctx.parent = tempParent as EntryData;
590
825
 
591
- const result = use()?.flat(3);
826
+ const result = withParent(ctx, tempParent as EntryData, () =>
827
+ mergedUse()?.flat(3),
828
+ );
592
829
 
593
830
  // Copy cache config only if cache() was called during the use() callback.
594
- // The spread from originalParent may carry an inherited .cache from
595
- // a parent cache() boundary — only copy if it was newly set.
831
+ // The spread may carry an inherited .cache from a parent cache() boundary —
832
+ // only copy if it was newly set.
596
833
  if (
597
834
  (tempParent as any).cache &&
598
835
  (tempParent as any).cache !== parentCache
@@ -600,13 +837,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
600
837
  (loaderEntry as any).cache = (tempParent as any).cache;
601
838
  }
602
839
 
603
- // Restore original parent
604
- ctx.parent = originalParent;
605
-
606
- invariant(
607
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
608
- `loader() use() callback must return an array of use items [${name}]`,
609
- );
840
+ validateUseItems(result, name, "loader", "use");
610
841
  }
611
842
 
612
843
  ctx.parent.loader.push(loaderEntry);
@@ -617,21 +848,25 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
617
848
  * Loading helper - attaches a loading component to the current entry
618
849
  * Loading components are static (no context) and shown during navigation
619
850
  */
620
- const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
621
- const store = getContext();
622
- const ctx = store.getStore();
623
- if (!ctx) throw new Error("loading() must be called inside map()");
851
+ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
852
+ const { store, ctx } = requireDslContext(
853
+ "loading() must be called inside urls()",
854
+ );
624
855
 
625
856
  const parent = ctx.parent;
626
857
  if (!parent || !("loading" in parent)) {
627
858
  invariant(false, "No parent entry available for loading()");
628
859
  }
629
860
 
861
+ // Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
862
+ const resolved =
863
+ typeof component === "function" ? (component as () => any)() : component;
864
+
630
865
  // If ssr: false and we're in SSR, set loading to false
631
866
  if (options?.ssr === false && ctx.isSSR) {
632
867
  parent.loading = false;
633
868
  } else {
634
- parent.loading = component;
869
+ parent.loading = resolved;
635
870
  }
636
871
 
637
872
  const name = `$${store.getNextIndex("loading")}`;
@@ -639,10 +874,13 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
639
874
  };
640
875
 
641
876
  /**
642
- * Transition helper - attaches a ViewTransition config to the current entry
643
- * or wraps a group of routes in a transparent layout with ViewTransition
877
+ * Transition helper - opts the entry (or a wrapped group of routes) into
878
+ * transition-driven navigation by attaching a TransitionConfig. This drives the
879
+ * commit through startTransition (content hold on all React versions) and, on
880
+ * experimental React, places a `<ViewTransition>` boundary unless
881
+ * `viewTransition: false`. See skills/view-transitions for the matrix.
644
882
  */
645
- const transitionFn = (
883
+ const transition = (
646
884
  configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
647
885
  maybeChildren?: () => UseItems<AllUseItems>,
648
886
  ): TransitionItem => {
@@ -656,11 +894,15 @@ const transitionFn = (
656
894
  const children: (() => UseItems<AllUseItems>) | undefined =
657
895
  typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
658
896
 
659
- const store = getContext();
660
- const ctx = store.getStore();
661
- if (!ctx) throw new Error("transition() must be called inside map()");
897
+ const { store, ctx } = requireDslContext(
898
+ "transition() must be called inside urls()",
899
+ );
662
900
 
663
- const name = `$${store.getNextIndex("transition")}`;
901
+ // Allocate a single index for this transition() call (used in all paths),
902
+ // mirroring cache() — the child form uses it for the name, the wrapper form
903
+ // reuses it for the namespace, so no index is burned.
904
+ const transitionIndex = store.getNextIndex("transition");
905
+ const name = `$${transitionIndex}`;
664
906
 
665
907
  if (!children) {
666
908
  // Position 1: child of path() — attach to parent entry
@@ -673,70 +915,51 @@ const transitionFn = (
673
915
  }
674
916
 
675
917
  // Position 2: wrapper — create a transparent layout with transition config
676
- const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
918
+ const namespace = `${ctx.namespace}.${transitionIndex}`;
677
919
  const entry = {
920
+ ...emptySegmentBase(),
678
921
  id: namespace,
679
922
  shortCode: store.getShortCode("layout"),
680
923
  type: "layout",
681
924
  parent: ctx.parent,
682
925
  handler: RootLayout,
683
- loading: undefined,
684
926
  transition: config,
685
- middleware: [],
686
- revalidate: [],
687
- errorBoundary: [],
688
- notFoundBoundary: [],
689
- layout: [],
690
- parallel: [],
691
- intercept: [],
692
- loader: [],
693
- } as EntryData;
694
-
695
- const result = store.run(namespace, entry, children)?.flat(3);
927
+ } satisfies EntryData;
696
928
 
697
- invariant(
698
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
699
- `transition() children callback must return an array of use items [${namespace}]`,
929
+ const result = runAndValidateUseItems(
930
+ store,
931
+ namespace,
932
+ entry,
933
+ children,
934
+ "transition",
935
+ "children",
700
936
  );
701
937
 
702
- const hasRoutes =
703
- result &&
704
- Array.isArray(result) &&
705
- result.some((item) => hasRoutesInItem(item));
706
-
707
- if (!hasRoutes) {
708
- const parent = ctx.parent;
709
- if (parent && "layout" in parent) {
710
- entry.parent = null;
711
- parent.layout.push(entry);
712
- }
713
- }
938
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
714
939
 
715
940
  return { name: namespace, type: "transition" } as TransitionItem;
716
941
  };
717
942
 
718
- const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
719
- const store = getContext();
720
- const ctx = store.getStore();
721
- if (!ctx) throw new Error("route() must be called inside map()");
943
+ const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
944
+ const { store, ctx } = requireDslContext(
945
+ "route() must be called inside urls()",
946
+ );
947
+
948
+ // Reject names colliding with reserved internal prefixes ($path_, $prefix_),
949
+ // the same guard path() and include() enforce. Without it such a name
950
+ // type-checks on an untyped router, then silently vanishes from generated
951
+ // route-types and public reverse() (isAutoGeneratedRouteName filters it out).
952
+ validateUserRouteName(name);
722
953
 
723
954
  const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
724
955
 
725
956
  const entry = {
957
+ ...emptySegmentBase(),
726
958
  id: namespace,
727
959
  shortCode: store.getShortCode("route"),
728
960
  type: "route",
729
961
  parent: ctx.parent,
730
- handler,
731
- loading: undefined, // Allow loading() to attach loading state
732
- middleware: [],
733
- revalidate: [],
734
- errorBoundary: [],
735
- notFoundBoundary: [],
736
- layout: [],
737
- parallel: [],
738
- intercept: [],
739
- loader: [],
962
+ handler: handler as unknown as Handler<any, any, any>,
740
963
  } satisfies EntryData;
741
964
 
742
965
  /* We will throw if user is registring same route name twice */
@@ -746,12 +969,18 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
746
969
  );
747
970
  /* Register route entry */
748
971
  ctx.manifest.set(name, entry);
972
+ /* Merge handler.use defaults with explicit use */
973
+ const handlerUseFn = resolveHandlerUse(handler);
974
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
749
975
  /* Run use and attach handlers */
750
- if (use && typeof use === "function") {
751
- const result = store.run(namespace, entry, use)?.flat(3);
752
- invariant(
753
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
754
- `route() use() callback must return an array of use items [${namespace}]`,
976
+ if (mergedUse) {
977
+ const result = runAndValidateUseItems(
978
+ store,
979
+ namespace,
980
+ entry,
981
+ mergedUse,
982
+ "route",
983
+ "use",
755
984
  );
756
985
  return { name: namespace, type: "route", uses: result } as RouteItem;
757
986
  }
@@ -761,9 +990,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
761
990
  };
762
991
 
763
992
  const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
764
- const store = getContext();
765
- const ctx = store.getStore();
766
- if (!ctx) throw new Error("layout() must be called inside map()");
993
+ const { store, ctx } = requireDslContext(
994
+ "layout() must be called inside urls()",
995
+ );
767
996
 
768
997
  invariant(
769
998
  !ctx.parent || ctx.parent.type !== "parallel",
@@ -781,20 +1010,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
781
1010
 
782
1011
  const urlPrefix = getUrlPrefix();
783
1012
  const entry = {
1013
+ ...emptySegmentBase(),
784
1014
  id: namespace,
785
1015
  shortCode,
786
1016
  type: "layout",
787
1017
  parent: ctx.parent,
788
1018
  handler: unwrappedHandler,
789
- loading: undefined, // Allow loading() to attach loading state
790
- middleware: [],
791
- revalidate: [],
792
- errorBoundary: [],
793
- notFoundBoundary: [],
794
- parallel: [],
795
- intercept: [],
796
- layout: [],
797
- loader: [],
798
1019
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
799
1020
  ...(isStatic
800
1021
  ? {
@@ -809,14 +1030,20 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
809
1030
  (handler as any).$$routePrefix = ctx.namePrefix;
810
1031
  }
811
1032
 
812
- // Run use callback if provided
813
- let result: AllUseItems[] | undefined;
814
- if (use && typeof use === "function") {
815
- result = store.run(namespace, entry, use)?.flat(3);
1033
+ // Merge handler.use defaults with explicit use
1034
+ const handlerUseFn = resolveHandlerUse(handler);
1035
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "layout");
816
1036
 
817
- invariant(
818
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
819
- `layout() use() callback must return an array of use items [${namespace}]`,
1037
+ // Run merged use callback if present
1038
+ let result: AllUseItems[] | undefined;
1039
+ if (mergedUse) {
1040
+ result = runAndValidateUseItems(
1041
+ store,
1042
+ namespace,
1043
+ entry,
1044
+ mergedUse,
1045
+ "layout",
1046
+ "use",
820
1047
  );
821
1048
  }
822
1049
 
@@ -858,9 +1085,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
858
1085
  `Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
859
1086
  );
860
1087
 
861
- // Clear parent pointer for orphan layouts to prevent duplicate processing
862
- entry.parent = null;
863
- parent.layout.push(entry);
1088
+ attachOrphanSibling(parent, entry);
864
1089
  }
865
1090
  }
866
1091
 
@@ -873,33 +1098,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
873
1098
  } as LayoutItem;
874
1099
  };
875
1100
 
876
- const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
877
- return (
878
- typeof item === "undefined" ||
879
- item === null ||
880
- (item &&
881
- typeof item === "object" &&
882
- "type" in item &&
883
- [
884
- "layout",
885
- "route",
886
- "middleware",
887
- "revalidate",
888
- "parallel",
889
- "intercept",
890
- "loader",
891
- "loading",
892
- "errorBoundary",
893
- "notFoundBoundary",
894
- "when",
895
- "cache",
896
- "transition",
897
- "include", // For urls() include() helper
898
- ].includes(item.type))
899
- );
900
- };
1101
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
1102
+ item == null ||
1103
+ (typeof item === "object" &&
1104
+ "type" in item &&
1105
+ ALL_USE_ITEM_TYPES.has(item.type));
901
1106
 
902
- // Global helper exports for direct import from @rangojs/router
1107
+ // DSL helpers exported for direct import from @rangojs/router and for
1108
+ // assembly into the RouteHelpers object in helper-factories.ts. The route-item
1109
+ // types are discriminated by their `type` literal, so the helpers carry no brand.
903
1110
  export {
904
1111
  layout,
905
1112
  cache,
@@ -910,25 +1117,11 @@ export {
910
1117
  when,
911
1118
  errorBoundary,
912
1119
  notFoundBoundary,
913
- loaderFn as loader,
914
- loadingFn as loading,
915
- transitionFn as transition,
916
- };
917
-
918
- const isOrphanLayout = (item: AllUseItems): boolean => {
919
- return (
920
- item.type === "layout" &&
921
- !item.uses?.some((child) => hasRoutesInItem(child))
922
- );
923
- };
924
-
925
- // Internal exports used by helper-factories.ts
926
- export {
927
- routeFn,
928
- loaderFn,
929
- loadingFn,
930
- transitionFn,
931
- hasRoutesInItem,
1120
+ route,
1121
+ loader,
1122
+ loading,
1123
+ transition,
932
1124
  isValidUseItem,
933
- isOrphanLayout,
1125
+ emptySegmentBase,
1126
+ runAndValidateUseItems,
934
1127
  };