@rangojs/router 0.0.0-experimental.77 → 0.0.0-experimental.77ed8945

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 (239) hide show
  1. package/README.md +120 -25
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/vite/index.js +2103 -861
  4. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  5. package/package.json +13 -8
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +220 -30
  10. package/skills/caching/SKILL.md +116 -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 +229 -20
  16. package/skills/host-router/SKILL.md +66 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +26 -4
  19. package/skills/layout/SKILL.md +6 -7
  20. package/skills/links/SKILL.md +247 -17
  21. package/skills/loader/SKILL.md +219 -9
  22. package/skills/middleware/SKILL.md +47 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +12 -6
  28. package/skills/prerender/SKILL.md +14 -33
  29. package/skills/rango/SKILL.md +238 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +122 -47
  32. package/skills/route/SKILL.md +33 -4
  33. package/skills/router-setup/SKILL.md +3 -3
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/tailwind/SKILL.md +27 -3
  37. package/skills/typesafety/SKILL.md +319 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +116 -0
  42. package/src/browser/action-coordinator.ts +53 -36
  43. package/src/browser/app-shell.ts +39 -0
  44. package/src/browser/event-controller.ts +86 -70
  45. package/src/browser/history-state.ts +21 -0
  46. package/src/browser/index.ts +3 -3
  47. package/src/browser/navigation-bridge.ts +29 -9
  48. package/src/browser/navigation-client.ts +99 -77
  49. package/src/browser/navigation-store.ts +7 -8
  50. package/src/browser/navigation-transaction.ts +10 -28
  51. package/src/browser/partial-update.ts +60 -40
  52. package/src/browser/prefetch/cache.ts +196 -49
  53. package/src/browser/prefetch/fetch.ts +203 -59
  54. package/src/browser/prefetch/queue.ts +36 -5
  55. package/src/browser/rango-state.ts +37 -13
  56. package/src/browser/react/Link.tsx +18 -13
  57. package/src/browser/react/NavigationProvider.tsx +75 -31
  58. package/src/browser/react/filter-segment-order.ts +51 -7
  59. package/src/browser/react/index.ts +3 -0
  60. package/src/browser/react/location-state-shared.ts +175 -4
  61. package/src/browser/react/location-state.ts +39 -13
  62. package/src/browser/react/use-handle.ts +17 -9
  63. package/src/browser/react/use-navigation.ts +22 -2
  64. package/src/browser/react/use-params.ts +20 -8
  65. package/src/browser/react/use-reverse.ts +106 -0
  66. package/src/browser/react/use-router.ts +23 -2
  67. package/src/browser/react/use-segments.ts +11 -8
  68. package/src/browser/response-adapter.ts +52 -1
  69. package/src/browser/rsc-router.tsx +71 -22
  70. package/src/browser/scroll-restoration.ts +22 -14
  71. package/src/browser/segment-reconciler.ts +10 -14
  72. package/src/browser/segment-structure-assert.ts +2 -2
  73. package/src/browser/server-action-bridge.ts +44 -30
  74. package/src/browser/types.ts +12 -2
  75. package/src/build/collect-fallback-refs.ts +107 -0
  76. package/src/build/generate-manifest.ts +60 -35
  77. package/src/build/generate-route-types.ts +2 -0
  78. package/src/build/index.ts +8 -1
  79. package/src/build/prefix-tree-utils.ts +123 -0
  80. package/src/build/route-trie.ts +45 -1
  81. package/src/build/route-types/codegen.ts +4 -4
  82. package/src/build/route-types/include-resolution.ts +1 -1
  83. package/src/build/route-types/per-module-writer.ts +7 -4
  84. package/src/build/route-types/router-processing.ts +55 -14
  85. package/src/build/route-types/scan-filter.ts +1 -1
  86. package/src/build/route-types/source-scan.ts +118 -0
  87. package/src/build/runtime-discovery.ts +9 -20
  88. package/src/cache/cache-runtime.ts +17 -5
  89. package/src/cache/cache-scope.ts +51 -49
  90. package/src/cache/cf/cf-cache-store.ts +502 -32
  91. package/src/cache/cf/index.ts +3 -0
  92. package/src/cache/handle-snapshot.ts +103 -0
  93. package/src/cache/index.ts +3 -0
  94. package/src/cache/memory-segment-store.ts +3 -2
  95. package/src/cache/types.ts +10 -6
  96. package/src/client.rsc.tsx +3 -0
  97. package/src/client.tsx +96 -205
  98. package/src/context-var.ts +5 -5
  99. package/src/decode-loader-results.ts +36 -0
  100. package/src/errors.ts +30 -4
  101. package/src/handle.ts +4 -6
  102. package/src/host/index.ts +2 -2
  103. package/src/host/router.ts +129 -57
  104. package/src/host/types.ts +31 -2
  105. package/src/host/utils.ts +1 -1
  106. package/src/href-client.ts +140 -21
  107. package/src/index.rsc.ts +10 -6
  108. package/src/index.ts +17 -8
  109. package/src/loader-store.ts +500 -0
  110. package/src/loader.rsc.ts +2 -5
  111. package/src/loader.ts +3 -10
  112. package/src/missing-id-error.ts +68 -0
  113. package/src/outlet-context.ts +1 -1
  114. package/src/prerender/store.ts +9 -7
  115. package/src/prerender.ts +4 -4
  116. package/src/response-utils.ts +37 -0
  117. package/src/reverse.ts +65 -39
  118. package/src/route-content-wrapper.tsx +6 -28
  119. package/src/route-definition/dsl-helpers.ts +253 -265
  120. package/src/route-definition/helper-factories.ts +29 -139
  121. package/src/route-definition/helpers-types.ts +43 -15
  122. package/src/route-definition/resolve-handler-use.ts +6 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +26 -41
  125. package/src/router/content-negotiation.ts +15 -2
  126. package/src/router/error-handling.ts +1 -1
  127. package/src/router/find-match.ts +54 -6
  128. package/src/router/handler-context.ts +21 -41
  129. package/src/router/intercept-resolution.ts +4 -18
  130. package/src/router/lazy-includes.ts +41 -22
  131. package/src/router/loader-resolution.ts +82 -36
  132. package/src/router/manifest.ts +41 -19
  133. package/src/router/match-api.ts +4 -3
  134. package/src/router/match-handlers.ts +1 -0
  135. package/src/router/match-middleware/cache-lookup.ts +57 -95
  136. package/src/router/match-middleware/cache-store.ts +3 -2
  137. package/src/router/match-result.ts +53 -32
  138. package/src/router/metrics.ts +1 -1
  139. package/src/router/middleware-types.ts +15 -26
  140. package/src/router/middleware.ts +99 -84
  141. package/src/router/pattern-matching.ts +116 -19
  142. package/src/router/prerender-match.ts +40 -15
  143. package/src/router/preview-match.ts +3 -1
  144. package/src/router/request-classification.ts +40 -37
  145. package/src/router/revalidation.ts +58 -2
  146. package/src/router/router-interfaces.ts +51 -35
  147. package/src/router/router-options.ts +25 -1
  148. package/src/router/router-registry.ts +2 -5
  149. package/src/router/segment-resolution/fresh.ts +27 -6
  150. package/src/router/segment-resolution/revalidation.ts +147 -106
  151. package/src/router/segment-resolution/static-store.ts +19 -5
  152. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  153. package/src/router/substitute-pattern-params.ts +56 -0
  154. package/src/router/trie-matching.ts +40 -16
  155. package/src/router/types.ts +8 -0
  156. package/src/router/url-params.ts +49 -0
  157. package/src/router.ts +37 -25
  158. package/src/rsc/handler-context.ts +2 -2
  159. package/src/rsc/handler.ts +58 -77
  160. package/src/rsc/helpers.ts +72 -43
  161. package/src/rsc/index.ts +1 -1
  162. package/src/rsc/manifest-init.ts +28 -41
  163. package/src/rsc/origin-guard.ts +30 -10
  164. package/src/rsc/progressive-enhancement.ts +4 -0
  165. package/src/rsc/response-error.ts +79 -12
  166. package/src/rsc/response-route-handler.ts +76 -61
  167. package/src/rsc/rsc-rendering.ts +45 -51
  168. package/src/rsc/runtime-warnings.ts +9 -10
  169. package/src/rsc/server-action.ts +33 -39
  170. package/src/rsc/ssr-setup.ts +16 -0
  171. package/src/rsc/types.ts +8 -2
  172. package/src/search-params.ts +4 -4
  173. package/src/segment-content-promise.ts +67 -0
  174. package/src/segment-loader-promise.ts +122 -0
  175. package/src/segment-system.tsx +132 -116
  176. package/src/serialize.ts +243 -0
  177. package/src/server/context.ts +175 -53
  178. package/src/server/cookie-store.ts +28 -4
  179. package/src/server/request-context.ts +57 -51
  180. package/src/ssr/index.tsx +5 -1
  181. package/src/static-handler.ts +1 -1
  182. package/src/types/global-namespace.ts +39 -26
  183. package/src/types/handler-context.ts +68 -50
  184. package/src/types/index.ts +1 -0
  185. package/src/types/loader-types.ts +11 -9
  186. package/src/types/request-scope.ts +126 -0
  187. package/src/types/route-entry.ts +11 -0
  188. package/src/types/segments.ts +35 -2
  189. package/src/urls/include-helper.ts +34 -67
  190. package/src/urls/index.ts +1 -5
  191. package/src/urls/path-helper-types.ts +17 -3
  192. package/src/urls/path-helper.ts +17 -52
  193. package/src/urls/pattern-types.ts +36 -19
  194. package/src/urls/response-types.ts +22 -29
  195. package/src/urls/type-extraction.ts +58 -139
  196. package/src/urls/urls-function.ts +1 -5
  197. package/src/use-loader.tsx +413 -42
  198. package/src/vite/debug.ts +185 -0
  199. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  200. package/src/vite/discovery/discover-routers.ts +106 -75
  201. package/src/vite/discovery/discovery-errors.ts +194 -0
  202. package/src/vite/discovery/gate-state.ts +171 -0
  203. package/src/vite/discovery/prerender-collection.ts +72 -31
  204. package/src/vite/discovery/route-types-writer.ts +40 -84
  205. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  206. package/src/vite/discovery/state.ts +33 -0
  207. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  208. package/src/vite/index.ts +2 -0
  209. package/src/vite/plugin-types.ts +67 -0
  210. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  211. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  212. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  213. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  214. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  215. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  216. package/src/vite/plugins/expose-action-id.ts +54 -30
  217. package/src/vite/plugins/expose-id-utils.ts +12 -8
  218. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  219. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  220. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  221. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  222. package/src/vite/plugins/expose-internal-ids.ts +496 -486
  223. package/src/vite/plugins/performance-tracks.ts +29 -25
  224. package/src/vite/plugins/use-cache-transform.ts +65 -50
  225. package/src/vite/plugins/version-injector.ts +39 -23
  226. package/src/vite/plugins/version-plugin.ts +59 -2
  227. package/src/vite/plugins/virtual-entries.ts +2 -2
  228. package/src/vite/rango.ts +116 -29
  229. package/src/vite/router-discovery.ts +753 -104
  230. package/src/vite/utils/ast-handler-extract.ts +15 -15
  231. package/src/vite/utils/banner.ts +1 -1
  232. package/src/vite/utils/bundle-analysis.ts +4 -2
  233. package/src/vite/utils/client-chunks.ts +190 -0
  234. package/src/vite/utils/forward-user-plugins.ts +193 -0
  235. package/src/vite/utils/manifest-utils.ts +8 -59
  236. package/src/vite/utils/package-resolution.ts +41 -1
  237. package/src/vite/utils/prerender-utils.ts +5 -4
  238. package/src/vite/utils/shared-utils.ts +107 -26
  239. package/src/browser/action-response-classifier.ts +0 -99
@@ -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
  };
@@ -217,9 +314,9 @@ const cache: RouteHelpers<any, any>["cache"] = (
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;
@@ -232,7 +329,7 @@ const cache: RouteHelpers<any, any>["cache"] = (
232
329
  } else if (typeof optionsOrChildren === "string") {
233
330
  // cache('profileName') or cache('profileName', () => [...])
234
331
  // Resolve from context-scoped profiles (set per-router via HelperContext).
235
- const ctxStore = RSCRouterContext.getStore();
332
+ const ctxStore = RangoContext.getStore();
236
333
  const profile = ctxStore?.cacheProfiles?.[optionsOrChildren];
237
334
  invariant(
238
335
  profile,
@@ -271,26 +368,18 @@ const cache: RouteHelpers<any, any>["cache"] = (
271
368
  // Create orphan cache entry (like orphan layout)
272
369
  // Subsequent siblings in the same array will attach to this entry
273
370
  const namespace = `${ctx.namespace}.${cacheIndex}`;
274
- const cacheUrlPrefix = getUrlPrefix();
371
+ const urlPrefix = getUrlPrefix();
275
372
 
276
373
  const entry = {
374
+ ...emptySegmentBase(),
277
375
  id: namespace,
278
376
  shortCode: store.getShortCode("cache"),
279
377
  type: "cache",
280
378
  parent: parent, // link to current parent for hierarchy
281
379
  cache: cacheConfig,
282
380
  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;
381
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
382
+ } satisfies EntryData;
294
383
 
295
384
  // Attach to parent's layout array (cache entries are structural like layouts)
296
385
  if (parent && "layout" in parent) {
@@ -304,13 +393,23 @@ const cache: RouteHelpers<any, any>["cache"] = (
304
393
  return { name: namespace, type: "cache" } as CacheItem;
305
394
  }
306
395
 
396
+ // Inside a loader() use() callback, only the direct form — cache()/cache(opts)/
397
+ // cache("profile") — writes cache config to the loader entry. The wrapper
398
+ // form creates a structural cache boundary with its own children scope, which
399
+ // has no effect on the loader and would silently no-op.
400
+ invariant(
401
+ !(ctx.parent && (ctx.parent as any).type === "loader"),
402
+ "cache() wrapper form is not valid inside loader() use(). Use cache({...}) without children to configure the loader's cache.",
403
+ );
404
+
307
405
  // With children: create a cache entry (like layout with caching semantics)
308
406
  const namespace = `${ctx.namespace}.${cacheIndex}`;
309
407
  const cacheShortCode = store.getShortCode("cache");
310
408
 
311
- const cacheUrlPrefix2 = getUrlPrefix();
409
+ const urlPrefix = getUrlPrefix();
312
410
 
313
411
  const entry = {
412
+ ...emptySegmentBase(),
314
413
  id: namespace,
315
414
  shortCode: cacheShortCode,
316
415
  type: "cache",
@@ -318,40 +417,22 @@ const cache: RouteHelpers<any, any>["cache"] = (
318
417
  cache: cacheConfig,
319
418
  // Cache entries render like layouts (with Outlet as default handler)
320
419
  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;
420
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
421
+ } satisfies EntryData;
332
422
 
333
423
  // 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}]`,
424
+ const result = runAndValidateUseItems(
425
+ store,
426
+ namespace,
427
+ entry,
428
+ children,
429
+ "cache",
430
+ "children",
339
431
  );
340
432
 
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
- }
433
+ // Cache entries are structural like layouts: with no routes inside, register
434
+ // as an orphan sibling.
435
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
355
436
 
356
437
  return { name: namespace, type: "cache", uses: result } as CacheItem;
357
438
  };
@@ -397,9 +478,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
397
478
  }
398
479
  }
399
480
 
400
- const store = getContext();
401
- const ctx = store.getStore();
402
- if (!ctx) throw new Error("middleware() must be called inside map()");
481
+ const { store, ctx } = requireDslContext(
482
+ "middleware() must be called inside urls()",
483
+ );
403
484
 
404
485
  if (!children) {
405
486
  // Sibling mode: attach to parent entry
@@ -418,22 +499,15 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
418
499
 
419
500
  const urlPrefix = getUrlPrefix();
420
501
  const entry = {
502
+ ...emptySegmentBase(),
421
503
  id: namespace,
422
504
  shortCode: store.getShortCode("layout"),
423
505
  type: "layout",
424
506
  parent: ctx.parent,
425
507
  handler: RootLayout,
426
- loading: undefined,
427
508
  middleware: [...fns],
428
- revalidate: [],
429
- errorBoundary: [],
430
- notFoundBoundary: [],
431
- layout: [],
432
- parallel: {},
433
- intercept: [],
434
- loader: [],
435
509
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
436
- } as EntryData;
510
+ } satisfies EntryData;
437
511
 
438
512
  // Run children callback. If the second arg was actually a middleware fn
439
513
  // (old variadic form: middleware(mw1, mw2)), this will return a non-array
@@ -446,25 +520,14 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
446
520
  "To pass multiple middleware, use middleware([fn1, fn2]).",
447
521
  );
448
522
 
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}]`,
523
+ const result = validateUseItems(
524
+ rawResult.flat(3),
525
+ namespace,
526
+ "middleware",
527
+ "children",
454
528
  );
455
529
 
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
- }
530
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
468
531
 
469
532
  return {
470
533
  name: namespace,
@@ -474,9 +537,9 @@ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
474
537
  };
475
538
 
476
539
  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()");
540
+ const { store, ctx } = requireDslContext(
541
+ "parallel() must be called inside urls()",
542
+ );
480
543
 
481
544
  if (!ctx.parent || !ctx.parent?.parallel) {
482
545
  invariant(false, "No parent entry available for parallel()");
@@ -528,20 +591,12 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
528
591
  // Create full EntryData for parallel with its own loaders/revalidate/loading
529
592
  const parallelUrlPrefix = getUrlPrefix();
530
593
  const entry = {
594
+ ...emptySegmentBase(),
531
595
  id: namespace,
532
596
  shortCode: store.getShortCode("parallel"),
533
597
  type: "parallel",
534
598
  parent: null, // Parallels don't participate in parent chain traversal
535
599
  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
600
  ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
546
601
  ...(hasStaticSlot
547
602
  ? {
@@ -596,10 +651,13 @@ const parallel: RouteHelpers<any, any>["parallel"] = (slots, use) => {
596
651
  "parallel",
597
652
  );
598
653
  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}]`,
654
+ runAndValidateUseItems(
655
+ store,
656
+ namespace,
657
+ slotEntry,
658
+ slotMergedUse,
659
+ "parallel",
660
+ "use",
603
661
  );
604
662
  }
605
663
 
@@ -639,9 +697,9 @@ const intercept = (
639
697
  handler: any,
640
698
  use?: () => any[],
641
699
  ) => {
642
- const store = getContext();
643
- const ctx = store.getStore();
644
- if (!ctx) throw new Error("intercept() must be called inside map()");
700
+ const { store, ctx } = requireDslContext(
701
+ "intercept() must be called inside urls()",
702
+ );
645
703
 
646
704
  if (!ctx.parent || !ctx.parent?.intercept) {
647
705
  invariant(false, "No parent entry available for intercept()");
@@ -680,15 +738,13 @@ const intercept = (
680
738
 
681
739
  // Run merged use callback to collect loaders, revalidate, middleware, etc.
682
740
  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
741
+ // Capture layout() calls into a temporary array
688
742
  const capturedLayouts: EntryData[] = [];
689
743
 
744
+ // Temporary parent so middleware/loader/revalidate/when attach to the
745
+ // intercept entry; the loading get/set accessor mirrors writes onto `entry`.
690
746
  const tempParent = {
691
- ...originalParent,
747
+ ...ctx.parent,
692
748
  middleware: entry.middleware,
693
749
  revalidate: entry.revalidate,
694
750
  errorBoundary: entry.errorBoundary,
@@ -696,7 +752,6 @@ const intercept = (
696
752
  loader: entry.loader,
697
753
  layout: capturedLayouts, // Capture layout() calls
698
754
  when: entry.when, // Capture when() conditions
699
- // Use getter/setter to capture loading on the entry
700
755
  get loading() {
701
756
  return entry.loading;
702
757
  },
@@ -704,12 +759,10 @@ const intercept = (
704
759
  entry.loading = value;
705
760
  },
706
761
  };
707
- ctx.parent = tempParent as EntryData;
708
-
709
- const result = mergedUse()?.flat(3);
710
762
 
711
- // Restore original parent
712
- ctx.parent = originalParent;
763
+ const result = withParent(ctx, tempParent as EntryData, () =>
764
+ mergedUse()?.flat(3),
765
+ );
713
766
 
714
767
  // Extract layout from captured layouts (use first one if multiple)
715
768
  // Layout inside intercept should always be ReactNode or Handler, not Record slots
@@ -719,10 +772,7 @@ const intercept = (
719
772
  | Handler<any, any, any>;
720
773
  }
721
774
 
722
- invariant(
723
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
724
- `intercept() use() callback must return an array of use items [${namespace}]`,
725
- );
775
+ validateUseItems(result, namespace, "intercept", "use");
726
776
  }
727
777
 
728
778
  ctx.parent.intercept.push(entry);
@@ -732,10 +782,10 @@ const intercept = (
732
782
  /**
733
783
  * Loader helper - attaches a loader to the current entry
734
784
  */
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()");
785
+ const loader: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
786
+ const { store, ctx } = requireDslContext(
787
+ "loader() must be called inside urls()",
788
+ );
739
789
 
740
790
  // Attach to last entry in stack
741
791
  if (!ctx.parent || !ctx.parent?.loader) {
@@ -750,25 +800,28 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
750
800
  revalidate: [] as ShouldRevalidateFn<any, any>[],
751
801
  };
752
802
 
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;
803
+ // Merge handler.use defaults (attached to the loader definition) with explicit use
804
+ const handlerUseFn = resolveHandlerUse(loaderDef);
805
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "loader");
806
+
807
+ // If any use callback is in effect, run it to collect revalidation rules and cache config
808
+ if (mergedUse) {
757
809
  // Create a temporary "parent" with type "loader" so cache() can detect it.
758
810
  // Save existing .cache to distinguish inherited config from newly set config.
759
- const parentCache = (originalParent as any).cache;
811
+ const parentCache = (ctx.parent as any).cache;
760
812
  const tempParent = {
761
- ...originalParent,
813
+ ...ctx.parent,
762
814
  type: "loader",
763
815
  revalidate: loaderEntry.revalidate,
764
816
  };
765
- ctx.parent = tempParent as EntryData;
766
817
 
767
- const result = use()?.flat(3);
818
+ const result = withParent(ctx, tempParent as EntryData, () =>
819
+ mergedUse()?.flat(3),
820
+ );
768
821
 
769
822
  // 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.
823
+ // The spread may carry an inherited .cache from a parent cache() boundary —
824
+ // only copy if it was newly set.
772
825
  if (
773
826
  (tempParent as any).cache &&
774
827
  (tempParent as any).cache !== parentCache
@@ -776,13 +829,7 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
776
829
  (loaderEntry as any).cache = (tempParent as any).cache;
777
830
  }
778
831
 
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
- );
832
+ validateUseItems(result, name, "loader", "use");
786
833
  }
787
834
 
788
835
  ctx.parent.loader.push(loaderEntry);
@@ -793,10 +840,10 @@ const loaderFn: RouteHelpers<any, any>["loader"] = (loaderDef, use) => {
793
840
  * Loading helper - attaches a loading component to the current entry
794
841
  * Loading components are static (no context) and shown during navigation
795
842
  */
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()");
843
+ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
844
+ const { store, ctx } = requireDslContext(
845
+ "loading() must be called inside urls()",
846
+ );
800
847
 
801
848
  const parent = ctx.parent;
802
849
  if (!parent || !("loading" in parent)) {
@@ -819,10 +866,13 @@ const loadingFn: RouteHelpers<any, any>["loading"] = (component, options) => {
819
866
  };
820
867
 
821
868
  /**
822
- * Transition helper - attaches a ViewTransition config to the current entry
823
- * or wraps a group of routes in a transparent layout with ViewTransition
869
+ * Transition helper - opts the entry (or a wrapped group of routes) into
870
+ * transition-driven navigation by attaching a TransitionConfig. This drives the
871
+ * commit through startTransition (content hold on all React versions) and, on
872
+ * experimental React, places a `<ViewTransition>` boundary unless
873
+ * `viewTransition: false`. See skills/view-transitions for the matrix.
824
874
  */
825
- const transitionFn = (
875
+ const transition = (
826
876
  configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
827
877
  maybeChildren?: () => UseItems<AllUseItems>,
828
878
  ): TransitionItem => {
@@ -836,9 +886,9 @@ const transitionFn = (
836
886
  const children: (() => UseItems<AllUseItems>) | undefined =
837
887
  typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
838
888
 
839
- const store = getContext();
840
- const ctx = store.getStore();
841
- if (!ctx) throw new Error("transition() must be called inside map()");
889
+ const { store, ctx } = requireDslContext(
890
+ "transition() must be called inside urls()",
891
+ );
842
892
 
843
893
  const name = `$${store.getNextIndex("transition")}`;
844
894
 
@@ -855,68 +905,43 @@ const transitionFn = (
855
905
  // Position 2: wrapper — create a transparent layout with transition config
856
906
  const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
857
907
  const entry = {
908
+ ...emptySegmentBase(),
858
909
  id: namespace,
859
910
  shortCode: store.getShortCode("layout"),
860
911
  type: "layout",
861
912
  parent: ctx.parent,
862
913
  handler: RootLayout,
863
- loading: undefined,
864
914
  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);
915
+ } satisfies EntryData;
876
916
 
877
- invariant(
878
- Array.isArray(result) && result.every((item) => isValidUseItem(item)),
879
- `transition() children callback must return an array of use items [${namespace}]`,
917
+ const result = runAndValidateUseItems(
918
+ store,
919
+ namespace,
920
+ entry,
921
+ children,
922
+ "transition",
923
+ "children",
880
924
  );
881
925
 
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
- }
926
+ if (isOrphan(result)) attachOrphanSibling(ctx.parent, entry);
894
927
 
895
928
  return { name: namespace, type: "transition" } as TransitionItem;
896
929
  };
897
930
 
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()");
931
+ const route: RouteHelpers<any, any>["route"] = (name, handler, use) => {
932
+ const { store, ctx } = requireDslContext(
933
+ "route() must be called inside urls()",
934
+ );
902
935
 
903
936
  const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
904
937
 
905
938
  const entry = {
939
+ ...emptySegmentBase(),
906
940
  id: namespace,
907
941
  shortCode: store.getShortCode("route"),
908
942
  type: "route",
909
943
  parent: ctx.parent,
910
944
  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
945
  } satisfies EntryData;
921
946
 
922
947
  /* We will throw if user is registring same route name twice */
@@ -931,10 +956,13 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
931
956
  const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
932
957
  /* Run use and attach handlers */
933
958
  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}]`,
959
+ const result = runAndValidateUseItems(
960
+ store,
961
+ namespace,
962
+ entry,
963
+ mergedUse,
964
+ "route",
965
+ "use",
938
966
  );
939
967
  return { name: namespace, type: "route", uses: result } as RouteItem;
940
968
  }
@@ -944,9 +972,9 @@ const routeFn: RouteHelpers<any, any>["route"] = (name, handler, use) => {
944
972
  };
945
973
 
946
974
  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()");
975
+ const { store, ctx } = requireDslContext(
976
+ "layout() must be called inside urls()",
977
+ );
950
978
 
951
979
  invariant(
952
980
  !ctx.parent || ctx.parent.type !== "parallel",
@@ -964,20 +992,12 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
964
992
 
965
993
  const urlPrefix = getUrlPrefix();
966
994
  const entry = {
995
+ ...emptySegmentBase(),
967
996
  id: namespace,
968
997
  shortCode,
969
998
  type: "layout",
970
999
  parent: ctx.parent,
971
1000
  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
1001
  ...(urlPrefix ? { mountPath: urlPrefix } : {}),
982
1002
  ...(isStatic
983
1003
  ? {
@@ -999,11 +1019,13 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
999
1019
  // Run merged use callback if present
1000
1020
  let result: AllUseItems[] | undefined;
1001
1021
  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}]`,
1022
+ result = runAndValidateUseItems(
1023
+ store,
1024
+ namespace,
1025
+ entry,
1026
+ mergedUse,
1027
+ "layout",
1028
+ "use",
1007
1029
  );
1008
1030
  }
1009
1031
 
@@ -1045,9 +1067,7 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1045
1067
  `Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
1046
1068
  );
1047
1069
 
1048
- // Clear parent pointer for orphan layouts to prevent duplicate processing
1049
- entry.parent = null;
1050
- parent.layout.push(entry);
1070
+ attachOrphanSibling(parent, entry);
1051
1071
  }
1052
1072
  }
1053
1073
 
@@ -1060,33 +1080,15 @@ const layout: RouteHelpers<any, any>["layout"] = (handler, use) => {
1060
1080
  } as LayoutItem;
1061
1081
  };
1062
1082
 
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
- };
1083
+ const isValidUseItem = (item: any): item is AllUseItems | undefined | null =>
1084
+ item == null ||
1085
+ (typeof item === "object" &&
1086
+ "type" in item &&
1087
+ ALL_USE_ITEM_TYPES.has(item.type));
1088
1088
 
1089
- // Global helper exports for direct import from @rangojs/router
1089
+ // DSL helpers exported for direct import from @rangojs/router and for
1090
+ // assembly into the RouteHelpers object in helper-factories.ts. The route-item
1091
+ // types are discriminated by their `type` literal, so the helpers carry no brand.
1090
1092
  export {
1091
1093
  layout,
1092
1094
  cache,
@@ -1097,25 +1099,11 @@ export {
1097
1099
  when,
1098
1100
  errorBoundary,
1099
1101
  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,
1102
+ route,
1103
+ loader,
1104
+ loading,
1105
+ transition,
1119
1106
  isValidUseItem,
1120
- isOrphanLayout,
1107
+ emptySegmentBase,
1108
+ runAndValidateUseItems,
1121
1109
  };