@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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