@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -11,12 +11,16 @@ 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";
18
22
  import { isCachedFunction } from "../cache/taint.js";
19
- import { RSCRouterContext } from "../server/context";
23
+ import { RangoContext } from "../server/context";
20
24
  import { isStaticHandler } from "../static-handler.js";
21
25
  import RootLayout from "../server/root-layout";
22
26
  import type {
@@ -38,6 +42,7 @@ import type {
38
42
  } from "../route-types.js";
39
43
  import type { RouteHelpers } from "./helpers-types.js";
40
44
  import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
45
+ import { ALL_USE_ITEM_TYPES } from "./use-item-types.js";
41
46
 
42
47
  /**
43
48
  * Check if an item contains routes (directly or inside nested structures like cache).
@@ -61,16 +66,105 @@ const hasRoutesInItem = (item: AllUseItems): boolean => {
61
66
  return false;
62
67
  };
63
68
 
69
+ /**
70
+ * Fresh empty collections shared by every from-scratch segment entry. Returns
71
+ * new arrays/objects per call so no two entries share mutable references.
72
+ * mountPath is intentionally NOT included here — each call site adds it from
73
+ * getUrlPrefix() where applicable: the route() and transition() helpers add
74
+ * none, while path() (which also builds a `type: "route"` entry) and the
75
+ * structural helpers (layout/cache/middleware/parallel) do.
76
+ */
77
+ const emptySegmentBase = (): EntryPropDatas &
78
+ EntryPropSegments & { loading: undefined } => ({
79
+ loading: undefined,
80
+ middleware: [],
81
+ revalidate: [],
82
+ errorBoundary: [],
83
+ notFoundBoundary: [],
84
+ layout: [],
85
+ parallel: {},
86
+ intercept: [],
87
+ loader: [],
88
+ });
89
+
90
+ /**
91
+ * Run a children/use callback as a nested scope, flatten the result, and assert
92
+ * every item is a valid use item. `kind` preserves the existing error wording
93
+ * ("use()" vs "children" callback).
94
+ */
95
+ function runAndValidateUseItems(
96
+ store: ReturnType<typeof getContext>,
97
+ namespace: string,
98
+ entry: EntryData,
99
+ cb: () => any,
100
+ label: string,
101
+ kind: "use" | "children",
102
+ ): AllUseItems[] {
103
+ const result = store.run(namespace, entry, cb)?.flat(3);
104
+ return validateUseItems(result, namespace, label, kind);
105
+ }
106
+
107
+ /** Assert an already-invoked, flattened callback result is a use-item array. */
108
+ function validateUseItems(
109
+ result: any,
110
+ namespace: string,
111
+ label: string,
112
+ kind: "use" | "children",
113
+ ): AllUseItems[] {
114
+ invariant(
115
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
116
+ `${label}() ${kind === "use" ? "use()" : "children"} callback must return an array of use items [${namespace}]`,
117
+ );
118
+ return result as AllUseItems[];
119
+ }
120
+
121
+ /** True when a children/use result contains no routes (directly or nested). */
122
+ const isOrphan = (result: AllUseItems[]): boolean =>
123
+ !result.some((item) => item != null && hasRoutesInItem(item));
124
+
125
+ /**
126
+ * Register a routeless structural entry as an orphan sibling: clear its parent
127
+ * pointer so it leaves the middleware/parent-pointer chain (LOAD-BEARING — see
128
+ * docs/tree-structure.md) and push it onto the parent's layout[] so it renders
129
+ * as a wrapper. Used by cache()/middleware()/transition(); layout() runs extra
130
+ * validation and registers inline.
131
+ */
132
+ const attachOrphanSibling = (
133
+ parent: EntryData | null,
134
+ entry: EntryData,
135
+ ): void => {
136
+ entry.parent = null;
137
+ if (parent && "layout" in parent) parent.layout.push(entry);
138
+ };
139
+
140
+ /**
141
+ * Run `fn` with `ctx.parent` temporarily redirected to `temp` — a satellite
142
+ * entry that captures the attachments declared by a use() callback — restoring
143
+ * the original parent afterward, including on throw. loader()/intercept() each
144
+ * build their own tempParent shape (intercept keeps a loading get/set accessor
145
+ * and a captured-layouts array); this only centralizes the save/restore.
146
+ */
147
+ function withParent<T>(ctx: HelperContext, temp: EntryData, fn: () => T): T {
148
+ const original = ctx.parent;
149
+ ctx.parent = temp;
150
+ try {
151
+ return fn();
152
+ } finally {
153
+ ctx.parent = original;
154
+ }
155
+ }
156
+
64
157
  const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
65
- const ctx = getContext().getStore();
66
- if (!ctx) throw new Error("revalidate() must be called inside map()");
158
+ const { store, ctx } = requireDslContext(
159
+ "revalidate() must be called inside urls()",
160
+ );
67
161
 
68
162
  // Attach to last entry in stack
69
163
  const parent = ctx.parent;
70
164
  if (!parent || !("revalidate" in parent)) {
71
165
  invariant(false, "No parent entry available for revalidate()");
72
166
  }
73
- const name = `$${getContext().getNextIndex("revalidate")}`;
167
+ const name = `$${store.getNextIndex("revalidate")}`;
74
168
  parent.revalidate.push(fn);
75
169
  return { name, type: "revalidate" } as RevalidateItem;
76
170
  };
@@ -108,15 +202,16 @@ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
108
202
  * ```
109
203
  */
110
204
  const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
111
- const ctx = getContext().getStore();
112
- if (!ctx) throw new Error("errorBoundary() must be called inside map()");
205
+ const { store, ctx } = requireDslContext(
206
+ "errorBoundary() must be called inside urls()",
207
+ );
113
208
 
114
209
  // Attach to parent entry in stack
115
210
  const parent = ctx.parent;
116
211
  if (!parent || !("errorBoundary" in parent)) {
117
212
  invariant(false, "No parent entry available for errorBoundary()");
118
213
  }
119
- const name = `$${getContext().getNextIndex("errorBoundary")}`;
214
+ const name = `$${store.getNextIndex("errorBoundary")}`;
120
215
  parent.errorBoundary.push(fallback);
121
216
  return { name, type: "errorBoundary" } as ErrorBoundaryItem;
122
217
  };
@@ -155,15 +250,16 @@ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
155
250
  const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
156
251
  fallback,
157
252
  ) => {
158
- const ctx = getContext().getStore();
159
- if (!ctx) throw new Error("notFoundBoundary() must be called inside map()");
253
+ const { store, ctx } = requireDslContext(
254
+ "notFoundBoundary() must be called inside urls()",
255
+ );
160
256
 
161
257
  // Attach to parent entry in stack
162
258
  const parent = ctx.parent;
163
259
  if (!parent || !("notFoundBoundary" in parent)) {
164
260
  invariant(false, "No parent entry available for notFoundBoundary()");
165
261
  }
166
- const name = `$${getContext().getNextIndex("notFoundBoundary")}`;
262
+ const name = `$${store.getNextIndex("notFoundBoundary")}`;
167
263
  parent.notFoundBoundary.push(fallback);
168
264
  return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
169
265
  };
@@ -177,8 +273,9 @@ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
177
273
  * for the intercept to activate.
178
274
  */
179
275
  const when: RouteHelpers<any, any>["when"] = (fn) => {
180
- const ctx = getContext().getStore();
181
- if (!ctx) throw new Error("when() must be called inside intercept()");
276
+ const { store, ctx } = requireDslContext(
277
+ "when() must be called inside intercept()",
278
+ );
182
279
 
183
280
  // The when() function needs to be captured by the intercept's tempParent
184
281
  // which should have a `when` array. If not present, we're not inside intercept()
@@ -190,7 +287,7 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
190
287
  );
191
288
  }
192
289
 
193
- const name = `$${getContext().getNextIndex("when")}`;
290
+ const name = `$${store.getNextIndex("when")}`;
194
291
  parent.when.push(fn);
195
292
  return { name, type: "when" } as WhenItem;
196
293
  };
@@ -205,21 +302,21 @@ const when: RouteHelpers<any, any>["when"] = (fn) => {
205
302
  * Supports these call signatures:
206
303
  * - cache() - no args, uses app-level defaults (for loader caching)
207
304
  * - cache(() => [...]) - wraps children with app-level defaults
208
- * - cache('profileName') - uses a named cache profile
209
- * - cache('profileName', () => [...]) - named profile with children
210
305
  * - cache({ ttl: 60 }, () => [...]) - with explicit options
306
+ *
307
+ * Named cache profiles are applied via the `"use cache: <profile>"` directive,
308
+ * not a `cache("profileName")` form in the route tree.
211
309
  */
212
310
  const cache: RouteHelpers<any, any>["cache"] = (
213
311
  optionsOrChildren?:
214
312
  | PartialCacheOptions
215
313
  | false
216
- | string
217
314
  | (() => UseItems<AllUseItems>),
218
315
  maybeChildren?: () => UseItems<AllUseItems>,
219
316
  ) => {
220
- const store = getContext();
221
- const ctx = store.getStore();
222
- if (!ctx) throw new Error("cache() must be called inside map()");
317
+ const { store, ctx } = requireDslContext(
318
+ "cache() must be called inside urls()",
319
+ );
223
320
 
224
321
  // Handle overloaded signature
225
322
  let options: PartialCacheOptions | false;
@@ -229,18 +326,6 @@ const cache: RouteHelpers<any, any>["cache"] = (
229
326
  // cache() - no args, use defaults
230
327
  options = {};
231
328
  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
329
  } else if (typeof optionsOrChildren === "function") {
245
330
  // cache(() => [...]) - use empty options (will use defaults)
246
331
  options = {};
@@ -271,26 +356,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
271
356
  // Create orphan cache entry (like orphan layout)
272
357
  // Subsequent siblings in the same array will attach to this entry
273
358
  const namespace = `${ctx.namespace}.${cacheIndex}`;
274
- const cacheUrlPrefix = getUrlPrefix();
359
+ const urlPrefix = getUrlPrefix();
275
360
 
276
361
  const entry = {
362
+ ...emptySegmentBase(),
277
363
  id: namespace,
278
364
  shortCode: store.getShortCode("cache"),
279
365
  type: "cache",
280
366
  parent: parent, // link to current parent for hierarchy
281
367
  cache: cacheConfig,
282
368
  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;
369
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
370
+ } satisfies EntryData;
294
371
 
295
372
  // Attach to parent's layout array (cache entries are structural like layouts)
296
373
  if (parent && "layout" in parent) {
@@ -304,10 +381,10 @@ const cache: RouteHelpers<any, any>["cache"] = (
304
381
  return { name: namespace, type: "cache" } as CacheItem;
305
382
  }
306
383
 
307
- // Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
308
- // cache("profile") — writes cache config to the loader entry. The wrapper
309
- // form creates a structural cache boundary with its own children scope, which
310
- // has no effect on the loader and would silently no-op.
384
+ // Inside a loader() use() callback, only the direct form — cache()/cache(opts)
385
+ // — writes cache config to the loader entry. The wrapper form creates a
386
+ // structural cache boundary with its own children scope, which has no effect
387
+ // on the loader and would silently no-op.
311
388
  invariant(
312
389
  !(ctx.parent && (ctx.parent as any).type === "loader"),
313
390
  "cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
@@ -317,9 +394,10 @@ const cache: RouteHelpers<any, any>["cache"] = (
317
394
  const namespace = `${ctx.namespace}.${cacheIndex}`;
318
395
  const cacheShortCode = store.getShortCode("cache");
319
396
 
320
- const cacheUrlPrefix2 = getUrlPrefix();
397
+ const urlPrefix = getUrlPrefix();
321
398
 
322
399
  const entry = {
400
+ ...emptySegmentBase(),
323
401
  id: namespace,
324
402
  shortCode: cacheShortCode,
325
403
  type: "cache",
@@ -327,40 +405,22 @@ const cache: RouteHelpers<any, any>["cache"] = (
327
405
  cache: cacheConfig,
328
406
  // Cache entries render like layouts (with Outlet as default handler)
329
407
  handler: RootLayout, // RootLayout just renders <Outlet />
330
- loading: undefined, // Allow loading() to attach loading state
331
- middleware: [],
332
- revalidate: [],
333
- errorBoundary: [],
334
- notFoundBoundary: [],
335
- layout: [],
336
- parallel: {},
337
- intercept: [],
338
- loader: [],
339
- ...(cacheUrlPrefix2 ? { mountPath: cacheUrlPrefix2 } : {}),
340
- } as EntryData;
408
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
409
+ } satisfies EntryData;
341
410
 
342
411
  // Run children with cache entry as parent
343
- const result = store.run(namespace, entry, children)?.flat(3);
344
-
345
- invariant(
346
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
347
- `cache() children callback must return an array of use items [${namespace}]`,
412
+ const result = runAndValidateUseItems(
413
+ store,
414
+ namespace,
415
+ entry,
416
+ children,
417
+ "cache",
418
+ "children",
348
419
  );
349
420
 
350
- // Check if this cache has routes (including nested caches/layouts)
351
- const hasRoutes =
352
- result &&
353
- Array.isArray(result) &&
354
- result.some((item) => hasRoutesInItem(item));
355
-
356
- if (!hasRoutes) {
357
- const parent = ctx.parent;
358
- if (parent && "layout" in parent) {
359
- // Attach to parent's layout array (cache entries are structural like layouts)
360
- entry.parent = null;
361
- parent.layout.push(entry);
362
- }
363
- }
421
+ // Cache entries are structural like layouts: with no routes inside, register
422
+ // as an orphan sibling.
423
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
364
424
 
365
425
  return { name: namespace, type: "cache", uses: result } as CacheItem;
366
426
  };
@@ -406,9 +466,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
406
466
  }
407
467
  }
408
468
 
409
- const store = getContext();
410
- const ctx = store.getStore();
411
- if (!ctx) throw new Error("middleware() must be called inside map()");
469
+ const { store, ctx } = requireDslContext(
470
+ "middleware() must be called inside urls()",
471
+ );
412
472
 
413
473
  if (!children) {
414
474
  // Sibling mode: attach to parent entry
@@ -427,22 +487,15 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
427
487
 
428
488
  const urlPrefix = getUrlPrefix();
429
489
  const entry = {
490
+ ...emptySegmentBase(),
430
491
  id: namespace,
431
492
  shortCode: store.getShortCode("layout"),
432
493
  type: "layout",
433
494
  parent: ctx.parent,
434
495
  handler: RootLayout,
435
- loading: undefined,
436
496
  middleware: [...fns],
437
- revalidate: [],
438
- errorBoundary: [],
439
- notFoundBoundary: [],
440
- layout: [],
441
- parallel: {},
442
- intercept: [],
443
- loader: [],
444
497
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
445
- } as EntryData;
498
+ } satisfies EntryData;
446
499
 
447
500
  // Run children callback. If the second arg was actually a middleware fn
448
501
  // (old variadic form: middleware(mw1, mw2)), this will return a non-array
@@ -455,25 +508,14 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
455
508
  "To pass multiple middleware, use middleware([fn1, fn2]).",
456
509
  );
457
510
 
458
- const result = rawResult.flat(3);
459
-
460
- invariant(
461
- result.every((item: any) => isValidUseItem(item)),
462
- `middleware() children callback must return an array of use items [${namespace}]`,
511
+ const result = validateUseItems(
512
+ rawResult.flat(3),
513
+ namespace,
514
+ "middleware",
515
+ "children",
463
516
  );
464
517
 
465
- const hasRoutes =
466
- result &&
467
- Array.isArray(result) &&
468
- result.some((item) => item != null && hasRoutesInItem(item));
469
-
470
- if (!hasRoutes) {
471
- const parent = ctx.parent;
472
- if (parent && "layout" in parent) {
473
- entry.parent = null;
474
- parent.layout.push(entry);
475
- }
476
- }
518
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
477
519
 
478
520
  return {
479
521
  name: namespace,
@@ -483,9 +525,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
483
525
  };
484
526
 
485
527
  const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
486
- const store = getContext();
487
- const ctx = store.getStore();
488
- if (!ctx) throw new Error("parallel() must be called inside map()");
528
+ const { store, ctx } = requireDslContext(
529
+ "parallel() must be called inside urls()",
530
+ );
489
531
 
490
532
  if (!ctx.parent || !ctx.parent?.parallel) {
491
533
  invariant(false, "No parent entry available for parallel()");
@@ -537,20 +579,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
537
579
  // Create full EntryData for parallel with its own loaders/revalidate/loading
538
580
  const parallelUrlPrefix = getUrlPrefix();
539
581
  const entry = {
582
+ ...emptySegmentBase(),
540
583
  id: namespace,
541
584
  shortCode: store.getShortCode("parallel"),
542
585
  type: "parallel",
543
586
  parent: null, // Parallels don't participate in parent chain traversal
544
587
  handler: unwrappedSlots,
545
- loading: undefined, // Allow loading() to attach loading state
546
- middleware: [],
547
- revalidate: [],
548
- errorBoundary: [],
549
- notFoundBoundary: [],
550
- layout: [],
551
- parallel: {},
552
- intercept: [],
553
- loader: [],
554
588
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
555
589
  ...(hasStaticSlot
556
590
  ? {
@@ -605,10 +639,13 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
605
639
  "parallel",
606
640
  );
607
641
  if (slotMergedUse) {
608
- const result = store.run(namespace, slotEntry, slotMergedUse)?.flat(3);
609
- invariant(
610
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
611
- `parallel() use() callback must return an array of use items [${namespace}]`,
642
+ runAndValidateUseItems(
643
+ store,
644
+ namespace,
645
+ slotEntry,
646
+ slotMergedUse,
647
+ "parallel",
648
+ "use",
612
649
  );
613
650
  }
614
651
 
@@ -648,9 +685,9 @@ const intercept = (
648
685
  handler: any,
649
686
  use?: () => any[],
650
687
  ) => {
651
- const store = getContext();
652
- const ctx = store.getStore();
653
- if (!ctx) throw new Error("intercept() must be called inside map()");
688
+ const { store, ctx } = requireDslContext(
689
+ "intercept() must be called inside urls()",
690
+ );
654
691
 
655
692
  if (!ctx.parent || !ctx.parent?.intercept) {
656
693
  invariant(false, "No parent entry available for intercept()");
@@ -689,15 +726,13 @@ const intercept = (
689
726
 
690
727
  // Run merged use callback to collect loaders, revalidate, middleware, etc.
691
728
  if (mergedUse) {
692
- // Create a temporary parent context for the use() callback
693
- // so that middleware, loader, revalidate attach to the intercept entry
694
- const originalParent = ctx.parent;
695
-
696
- // Capture layouts in a temporary array
729
+ // Capture layout() calls into a temporary array
697
730
  const capturedLayouts: EntryData[] = [];
698
731
 
732
+ // Temporary parent so middleware/loader/revalidate/when attach to the
733
+ // intercept entry; the loading get/set accessor mirrors writes onto `entry`.
699
734
  const tempParent = {
700
- ...originalParent,
735
+ ...ctx.parent,
701
736
  middleware: entry.middleware,
702
737
  revalidate: entry.revalidate,
703
738
  errorBoundary: entry.errorBoundary,
@@ -705,7 +740,6 @@ const intercept = (
705
740
  loader: entry.loader,
706
741
  layout: capturedLayouts, // Capture layout() calls
707
742
  when: entry.when, // Capture when() conditions
708
- // Use getter/setter to capture loading on the entry
709
743
  get loading() {
710
744
  return entry.loading;
711
745
  },
@@ -713,12 +747,10 @@ const intercept = (
713
747
  entry.loading = value;
714
748
  },
715
749
  };
716
- ctx.parent = tempParent as EntryData;
717
750
 
718
- const result = mergedUse()?.flat(3);
719
-
720
- // Restore original parent
721
- ctx.parent = originalParent;
751
+ const result = withParent(ctx, tempParent as EntryData, () =>
752
+ mergedUse()?.flat(3),
753
+ );
722
754
 
723
755
  // Extract layout from captured layouts (use first one if multiple)
724
756
  // Layout inside intercept should always be ReactNode or Handler, not Record slots
@@ -728,10 +760,7 @@ const intercept = (
728
760
  | Handler<any, any, any>;
729
761
  }
730
762
 
731
- invariant(
732
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
733
- `intercept() use() callback must return an array of use items [${namespace}]`,
734
- );
763
+ validateUseItems(result, namespace, "intercept", "use");
735
764
  }
736
765
 
737
766
  ctx.parent.intercept.push(entry);
@@ -741,10 +770,10 @@ const intercept = (
741
770
  /**
742
771
  * Loader helper - attaches a loader to the current entry
743
772
  */
744
- const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
745
- const store = getContext();
746
- const ctx = store.getStore();
747
- if (!ctx) throw new Error("loader() must be called inside map()");
773
+ const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
774
+ const { store, ctx } = requireDslContext(
775
+ "loader() must be called inside urls()",
776
+ );
748
777
 
749
778
  // Attach to last entry in stack
750
779
  if (!ctx.parent || !ctx.parent?.loader) {
@@ -765,23 +794,22 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
765
794
 
766
795
  // If any use callback is in effect, run it to collect revalidation rules and cache config
767
796
  if (mergedUse) {
768
- // Temporarily set context for revalidate()/cache() calls to target this loader
769
- const originalParent = ctx.parent;
770
797
  // Create a temporary "parent" with type "loader" so cache() can detect it.
771
798
  // Save existing .cache to distinguish inherited config from newly set config.
772
- const parentCache = (originalParent as any).cache;
799
+ const parentCache = (ctx.parent as any).cache;
773
800
  const tempParent = {
774
- ...originalParent,
801
+ ...ctx.parent,
775
802
  type: "loader",
776
803
  revalidate: loaderEntry.revalidate,
777
804
  };
778
- ctx.parent = tempParent as EntryData;
779
805
 
780
- const result = mergedUse()?.flat(3);
806
+ const result = withParent(ctx, tempParent as EntryData, () =>
807
+ mergedUse()?.flat(3),
808
+ );
781
809
 
782
810
  // Copy cache config only if cache() was called during the use() callback.
783
- // The spread from originalParent may carry an inherited .cache from
784
- // a parent cache() boundary — only copy if it was newly set.
811
+ // The spread may carry an inherited .cache from a parent cache() boundary —
812
+ // only copy if it was newly set.
785
813
  if (
786
814
  (tempParent as any).cache &&
787
815
  (tempParent as any).cache !== parentCache
@@ -789,13 +817,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
789
817
  (loaderEntry as any).cache = (tempParent as any).cache;
790
818
  }
791
819
 
792
- // Restore original parent
793
- ctx.parent = originalParent;
794
-
795
- invariant(
796
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
797
- `loader() use() callback must return an array of use items [${name}]`,
798
- );
820
+ validateUseItems(result, name, "loader", "use");
799
821
  }
800
822
 
801
823
  ctx.parent.loader.push(loaderEntry);
@@ -806,10 +828,10 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
806
828
  * Loading helper - attaches a loading component to the current entry
807
829
  * Loading components are static (no context) and shown during navigation
808
830
  */
809
- const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
810
- const store = getContext();
811
- const ctx = store.getStore();
812
- if (!ctx) throw new Error("loading() must be called inside map()");
831
+ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
832
+ const { store, ctx } = requireDslContext(
833
+ "loading() must be called inside urls()",
834
+ );
813
835
 
814
836
  const parent = ctx.parent;
815
837
  if (!parent || !("loading" in parent)) {
@@ -832,10 +854,13 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
832
854
  };
833
855
 
834
856
  /**
835
- * Transition helper - attaches a ViewTransition config to the current entry
836
- * or wraps a group of routes in a transparent layout with ViewTransition
857
+ * Transition helper - opts the entry (or a wrapped group of routes) into
858
+ * transition-driven navigation by attaching a TransitionConfig. This drives the
859
+ * commit through startTransition (content hold on all React versions) and, on
860
+ * experimental React, places a `<ViewTransition>` boundary unless
861
+ * `viewTransition: false`. See skills/view-transitions for the matrix.
837
862
  */
838
- const transitionFn = (
863
+ const transition = (
839
864
  configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
840
865
  maybeChildren?: () => UseItems<AllUseItems>,
841
866
  ): TransitionItem => {
@@ -849,9 +874,9 @@ const transitionFn = (
849
874
  const children: (() => UseItems<AllUseItems>) | undefined =
850
875
  typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
851
876
 
852
- const store = getContext();
853
- const ctx = store.getStore();
854
- if (!ctx) throw new Error("transition() must be called inside map()");
877
+ const { store, ctx } = requireDslContext(
878
+ "transition() must be called inside urls()",
879
+ );
855
880
 
856
881
  const name = `$${store.getNextIndex("transition")}`;
857
882
 
@@ -868,68 +893,43 @@ const transitionFn = (
868
893
  // Position 2: wrapper — create a transparent layout with transition config
869
894
  const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
870
895
  const entry = {
896
+ ...emptySegmentBase(),
871
897
  id: namespace,
872
898
  shortCode: store.getShortCode("layout"),
873
899
  type: "layout",
874
900
  parent: ctx.parent,
875
901
  handler: RootLayout,
876
- loading: undefined,
877
902
  transition: config,
878
- middleware: [],
879
- revalidate: [],
880
- errorBoundary: [],
881
- notFoundBoundary: [],
882
- layout: [],
883
- parallel: {},
884
- intercept: [],
885
- loader: [],
886
- } as EntryData;
887
-
888
- const result = store.run(namespace, entry, children)?.flat(3);
903
+ } satisfies EntryData;
889
904
 
890
- invariant(
891
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
892
- `transition() children callback must return an array of use items [${namespace}]`,
905
+ const result = runAndValidateUseItems(
906
+ store,
907
+ namespace,
908
+ entry,
909
+ children,
910
+ "transition",
911
+ "children",
893
912
  );
894
913
 
895
- const hasRoutes =
896
- result &&
897
- Array.isArray(result) &&
898
- result.some((item) => hasRoutesInItem(item));
899
-
900
- if (!hasRoutes) {
901
- const parent = ctx.parent;
902
- if (parent && "layout" in parent) {
903
- entry.parent = null;
904
- parent.layout.push(entry);
905
- }
906
- }
914
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
907
915
 
908
916
  return { name: namespace, type: "transition" } as TransitionItem;
909
917
  };
910
918
 
911
- const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
912
- const store = getContext();
913
- const ctx = store.getStore();
914
- if (!ctx) throw new Error("route() must be called inside map()");
919
+ const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
920
+ const { store, ctx } = requireDslContext(
921
+ "route() must be called inside urls()",
922
+ );
915
923
 
916
924
  const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
917
925
 
918
926
  const entry = {
927
+ ...emptySegmentBase(),
919
928
  id: namespace,
920
929
  shortCode: store.getShortCode("route"),
921
930
  type: "route",
922
931
  parent: ctx.parent,
923
932
  handler: handler as unknown as Handler<any, any, any>,
924
- loading: undefined, // Allow loading() to attach loading state
925
- middleware: [],
926
- revalidate: [],
927
- errorBoundary: [],
928
- notFoundBoundary: [],
929
- layout: [],
930
- parallel: {},
931
- intercept: [],
932
- loader: [],
933
933
  } satisfies EntryData;
934
934
 
935
935
  /* We will throw if user is registring same route name twice */
@@ -944,10 +944,13 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
944
944
  const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
945
945
  /* Run use and attach handlers */
946
946
  if (mergedUse) {
947
- const result = store.run(namespace, entry, mergedUse)?.flat(3);
948
- invariant(
949
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
950
- `route() use() callback must return an array of use items [${namespace}]`,
947
+ const result = runAndValidateUseItems(
948
+ store,
949
+ namespace,
950
+ entry,
951
+ mergedUse,
952
+ "route",
953
+ "use",
951
954
  );
952
955
  return { name: namespace, type: "route", uses: result } as RouteItem;
953
956
  }
@@ -957,9 +960,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
957
960
  };
958
961
 
959
962
  const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
960
- const store = getContext();
961
- const ctx = store.getStore();
962
- if (!ctx) throw new Error("layout() must be called inside map()");
963
+ const { store, ctx } = requireDslContext(
964
+ "layout() must be called inside urls()",
965
+ );
963
966
 
964
967
  invariant(
965
968
  !ctx.parent || ctx.parent.type !== "parallel",
@@ -977,20 +980,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
977
980
 
978
981
  const urlPrefix = getUrlPrefix();
979
982
  const entry = {
983
+ ...emptySegmentBase(),
980
984
  id: namespace,
981
985
  shortCode,
982
986
  type: "layout",
983
987
  parent: ctx.parent,
984
988
  handler: unwrappedHandler,
985
- loading: undefined, // Allow loading() to attach loading state
986
- middleware: [],
987
- revalidate: [],
988
- errorBoundary: [],
989
- notFoundBoundary: [],
990
- parallel: {},
991
- intercept: [],
992
- layout: [],
993
- loader: [],
994
989
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
995
990
  ...(isStatic
996
991
  ? {
@@ -1012,11 +1007,13 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1012
1007
  // Run merged use callback if present
1013
1008
  let result: AllUseItems[] | undefined;
1014
1009
  if (mergedUse) {
1015
- result = store.run(namespace, entry, mergedUse)?.flat(3);
1016
-
1017
- invariant(
1018
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
1019
- `layout() use() callback must return an array of use items [${namespace}]`,
1010
+ result = runAndValidateUseItems(
1011
+ store,
1012
+ namespace,
1013
+ entry,
1014
+ mergedUse,
1015
+ "layout",
1016
+ "use",
1020
1017
  );
1021
1018
  }
1022
1019
 
@@ -1058,9 +1055,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1058
1055
  `Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
1059
1056
  );
1060
1057
 
1061
- // Clear parent pointer for orphan layouts to prevent duplicate processing
1062
- entry.parent = null;
1063
- parent.layout.push(entry);
1058
+ attachOrphanSibling(parent, entry);
1064
1059
  }
1065
1060
  }
1066
1061
 
@@ -1073,33 +1068,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1073
1068
  } as LayoutItem;
1074
1069
  };
1075
1070
 
1076
- const isValidUseItem = (item: any): item is AllUseItems | undefined | null => {
1077
- return (
1078
- typeof item === "undefined" ||
1079
- item === null ||
1080
- (item &&
1081
- typeof item === "object" &&
1082
- "type" in item &&
1083
- [
1084
- "layout",
1085
- "route",
1086
- "middleware",
1087
- "revalidate",
1088
- "parallel",
1089
- "intercept",
1090
- "loader",
1091
- "loading",
1092
- "errorBoundary",
1093
- "notFoundBoundary",
1094
- "when",
1095
- "cache",
1096
- "transition",
1097
- "include", // For urls() include() helper
1098
- ].includes(item.type))
1099
- );
1100
- };
1071
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
1072
+ item == null ||
1073
+ (typeof item === "object" &&
1074
+ "type" in item &&
1075
+ ALL_USE_ITEM_TYPES.has(item.type));
1101
1076
 
1102
- // Global helper exports for direct import from @rangojs/router
1077
+ // DSL helpers exported for direct import from @rangojs/router and for
1078
+ // assembly into the RouteHelpers object in helper-factories.ts. The route-item
1079
+ // types are discriminated by their `type` literal, so the helpers carry no brand.
1103
1080
  export {
1104
1081
  layout,
1105
1082
  cache,
@@ -1110,25 +1087,11 @@ export {
1110
1087
  when,
1111
1088
  errorBoundary,
1112
1089
  notFoundBoundary,
1113
- loaderFn as loader,
1114
- loadingFn as loading,
1115
- transitionFn as transition,
1116
- };
1117
-
1118
- const isOrphanLayout = (item: AllUseItems): boolean => {
1119
- return (
1120
- item.type === "layout" &&
1121
- !item.uses?.some((child) => hasRoutesInItem(child))
1122
- );
1123
- };
1124
-
1125
- // Internal exports used by helper-factories.ts
1126
- export {
1127
- routeFn,
1128
- loaderFn,
1129
- loadingFn,
1130
- transitionFn,
1131
- hasRoutesInItem,
1090
+ route,
1091
+ loader,
1092
+ loading,
1093
+ transition,
1132
1094
  isValidUseItem,
1133
- isOrphanLayout,
1095
+ emptySegmentBase,
1096
+ runAndValidateUseItems,
1134
1097
  };