@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81

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 (316) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +5091 -941
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +61 -52
  7. package/skills/breadcrumbs/SKILL.md +250 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +167 -0
  14. package/skills/handler-use/SKILL.md +362 -0
  15. package/skills/hooks/SKILL.md +340 -72
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/intercept/SKILL.md +151 -8
  18. package/skills/layout/SKILL.md +122 -3
  19. package/skills/links/SKILL.md +92 -31
  20. package/skills/loader/SKILL.md +404 -44
  21. package/skills/middleware/SKILL.md +205 -37
  22. package/skills/migrate-nextjs/SKILL.md +560 -0
  23. package/skills/migrate-react-router/SKILL.md +765 -0
  24. package/skills/mime-routes/SKILL.md +128 -0
  25. package/skills/parallel/SKILL.md +263 -1
  26. package/skills/prerender/SKILL.md +685 -0
  27. package/skills/rango/SKILL.md +87 -16
  28. package/skills/response-routes/SKILL.md +411 -0
  29. package/skills/route/SKILL.md +281 -14
  30. package/skills/router-setup/SKILL.md +210 -32
  31. package/skills/tailwind/SKILL.md +129 -0
  32. package/skills/theme/SKILL.md +9 -8
  33. package/skills/typesafety/SKILL.md +328 -89
  34. package/skills/use-cache/SKILL.md +324 -0
  35. package/src/__internal.ts +102 -4
  36. package/src/bin/rango.ts +321 -0
  37. package/src/browser/action-coordinator.ts +97 -0
  38. package/src/browser/action-response-classifier.ts +99 -0
  39. package/src/browser/app-version.ts +14 -0
  40. package/src/browser/event-controller.ts +92 -64
  41. package/src/browser/history-state.ts +80 -0
  42. package/src/browser/intercept-utils.ts +52 -0
  43. package/src/browser/link-interceptor.ts +24 -4
  44. package/src/browser/logging.ts +55 -0
  45. package/src/browser/merge-segment-loaders.ts +20 -12
  46. package/src/browser/navigation-bridge.ts +317 -560
  47. package/src/browser/navigation-client.ts +206 -68
  48. package/src/browser/navigation-store.ts +73 -55
  49. package/src/browser/navigation-transaction.ts +297 -0
  50. package/src/browser/network-error-handler.ts +61 -0
  51. package/src/browser/partial-update.ts +343 -316
  52. package/src/browser/prefetch/cache.ts +216 -0
  53. package/src/browser/prefetch/fetch.ts +206 -0
  54. package/src/browser/prefetch/observer.ts +65 -0
  55. package/src/browser/prefetch/policy.ts +48 -0
  56. package/src/browser/prefetch/queue.ts +160 -0
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +112 -0
  59. package/src/browser/react/Link.tsx +253 -74
  60. package/src/browser/react/NavigationProvider.tsx +91 -11
  61. package/src/browser/react/context.ts +11 -0
  62. package/src/browser/react/filter-segment-order.ts +11 -0
  63. package/src/browser/react/index.ts +12 -12
  64. package/src/browser/react/location-state-shared.ts +95 -53
  65. package/src/browser/react/location-state.ts +60 -15
  66. package/src/browser/react/mount-context.ts +6 -1
  67. package/src/browser/react/nonce-context.ts +23 -0
  68. package/src/browser/react/shallow-equal.ts +27 -0
  69. package/src/browser/react/use-action.ts +29 -51
  70. package/src/browser/react/use-client-cache.ts +5 -3
  71. package/src/browser/react/use-handle.ts +30 -126
  72. package/src/browser/react/use-href.tsx +2 -2
  73. package/src/browser/react/use-link-status.ts +6 -5
  74. package/src/browser/react/use-navigation.ts +44 -65
  75. package/src/browser/react/use-params.ts +75 -0
  76. package/src/browser/react/use-pathname.ts +47 -0
  77. package/src/browser/react/use-router.ts +76 -0
  78. package/src/browser/react/use-search-params.ts +56 -0
  79. package/src/browser/react/use-segments.ts +80 -97
  80. package/src/browser/response-adapter.ts +73 -0
  81. package/src/browser/rsc-router.tsx +214 -58
  82. package/src/browser/scroll-restoration.ts +127 -52
  83. package/src/browser/segment-reconciler.ts +243 -0
  84. package/src/browser/segment-structure-assert.ts +16 -0
  85. package/src/browser/server-action-bridge.ts +510 -603
  86. package/src/browser/shallow.ts +6 -1
  87. package/src/browser/types.ts +141 -48
  88. package/src/browser/validate-redirect-origin.ts +29 -0
  89. package/src/build/generate-manifest.ts +235 -24
  90. package/src/build/generate-route-types.ts +39 -0
  91. package/src/build/index.ts +13 -0
  92. package/src/build/route-trie.ts +291 -0
  93. package/src/build/route-types/ast-helpers.ts +25 -0
  94. package/src/build/route-types/ast-route-extraction.ts +98 -0
  95. package/src/build/route-types/codegen.ts +102 -0
  96. package/src/build/route-types/include-resolution.ts +418 -0
  97. package/src/build/route-types/param-extraction.ts +48 -0
  98. package/src/build/route-types/per-module-writer.ts +128 -0
  99. package/src/build/route-types/router-processing.ts +618 -0
  100. package/src/build/route-types/scan-filter.ts +85 -0
  101. package/src/build/runtime-discovery.ts +231 -0
  102. package/src/cache/background-task.ts +34 -0
  103. package/src/cache/cache-key-utils.ts +44 -0
  104. package/src/cache/cache-policy.ts +125 -0
  105. package/src/cache/cache-runtime.ts +342 -0
  106. package/src/cache/cache-scope.ts +167 -309
  107. package/src/cache/cf/cf-cache-store.ts +571 -17
  108. package/src/cache/cf/index.ts +13 -3
  109. package/src/cache/document-cache.ts +116 -77
  110. package/src/cache/handle-capture.ts +81 -0
  111. package/src/cache/handle-snapshot.ts +41 -0
  112. package/src/cache/index.ts +1 -15
  113. package/src/cache/memory-segment-store.ts +191 -13
  114. package/src/cache/profile-registry.ts +73 -0
  115. package/src/cache/read-through-swr.ts +134 -0
  116. package/src/cache/segment-codec.ts +256 -0
  117. package/src/cache/taint.ts +153 -0
  118. package/src/cache/types.ts +72 -122
  119. package/src/client.rsc.tsx +3 -1
  120. package/src/client.tsx +135 -301
  121. package/src/component-utils.ts +4 -4
  122. package/src/components/DefaultDocument.tsx +5 -1
  123. package/src/context-var.ts +156 -0
  124. package/src/debug.ts +19 -9
  125. package/src/errors.ts +108 -2
  126. package/src/handle.ts +55 -29
  127. package/src/handles/MetaTags.tsx +73 -20
  128. package/src/handles/breadcrumbs.ts +66 -0
  129. package/src/handles/index.ts +1 -0
  130. package/src/handles/meta.ts +30 -13
  131. package/src/host/cookie-handler.ts +21 -15
  132. package/src/host/errors.ts +8 -8
  133. package/src/host/index.ts +4 -7
  134. package/src/host/pattern-matcher.ts +27 -27
  135. package/src/host/router.ts +61 -39
  136. package/src/host/testing.ts +8 -8
  137. package/src/host/types.ts +15 -7
  138. package/src/host/utils.ts +1 -1
  139. package/src/href-client.ts +119 -29
  140. package/src/index.rsc.ts +155 -19
  141. package/src/index.ts +251 -30
  142. package/src/internal-debug.ts +11 -0
  143. package/src/loader.rsc.ts +26 -157
  144. package/src/loader.ts +27 -10
  145. package/src/network-error-thrower.tsx +3 -1
  146. package/src/outlet-provider.tsx +45 -0
  147. package/src/prerender/param-hash.ts +37 -0
  148. package/src/prerender/store.ts +186 -0
  149. package/src/prerender.ts +524 -0
  150. package/src/reverse.ts +354 -0
  151. package/src/root-error-boundary.tsx +41 -29
  152. package/src/route-content-wrapper.tsx +7 -4
  153. package/src/route-definition/dsl-helpers.ts +1121 -0
  154. package/src/route-definition/helper-factories.ts +200 -0
  155. package/src/route-definition/helpers-types.ts +478 -0
  156. package/src/route-definition/index.ts +55 -0
  157. package/src/route-definition/redirect.ts +101 -0
  158. package/src/route-definition/resolve-handler-use.ts +149 -0
  159. package/src/route-definition.ts +1 -1428
  160. package/src/route-map-builder.ts +217 -123
  161. package/src/route-name.ts +53 -0
  162. package/src/route-types.ts +77 -8
  163. package/src/router/content-negotiation.ts +215 -0
  164. package/src/router/debug-manifest.ts +72 -0
  165. package/src/router/error-handling.ts +9 -9
  166. package/src/router/find-match.ts +160 -0
  167. package/src/router/handler-context.ts +438 -86
  168. package/src/router/intercept-resolution.ts +402 -0
  169. package/src/router/lazy-includes.ts +237 -0
  170. package/src/router/loader-resolution.ts +356 -128
  171. package/src/router/logging.ts +251 -0
  172. package/src/router/manifest.ts +163 -35
  173. package/src/router/match-api.ts +555 -0
  174. package/src/router/match-context.ts +5 -3
  175. package/src/router/match-handlers.ts +440 -0
  176. package/src/router/match-middleware/background-revalidation.ts +108 -93
  177. package/src/router/match-middleware/cache-lookup.ts +460 -10
  178. package/src/router/match-middleware/cache-store.ts +98 -26
  179. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  180. package/src/router/match-middleware/segment-resolution.ts +80 -6
  181. package/src/router/match-pipelines.ts +10 -45
  182. package/src/router/match-result.ts +135 -35
  183. package/src/router/metrics.ts +240 -15
  184. package/src/router/middleware-cookies.ts +55 -0
  185. package/src/router/middleware-types.ts +220 -0
  186. package/src/router/middleware.ts +324 -369
  187. package/src/router/navigation-snapshot.ts +182 -0
  188. package/src/router/pattern-matching.ts +211 -43
  189. package/src/router/prerender-match.ts +502 -0
  190. package/src/router/preview-match.ts +98 -0
  191. package/src/router/request-classification.ts +310 -0
  192. package/src/router/revalidation.ts +137 -38
  193. package/src/router/route-snapshot.ts +245 -0
  194. package/src/router/router-context.ts +41 -21
  195. package/src/router/router-interfaces.ts +484 -0
  196. package/src/router/router-options.ts +618 -0
  197. package/src/router/router-registry.ts +24 -0
  198. package/src/router/segment-resolution/fresh.ts +748 -0
  199. package/src/router/segment-resolution/helpers.ts +268 -0
  200. package/src/router/segment-resolution/loader-cache.ts +199 -0
  201. package/src/router/segment-resolution/revalidation.ts +1379 -0
  202. package/src/router/segment-resolution/static-store.ts +67 -0
  203. package/src/router/segment-resolution.ts +21 -0
  204. package/src/router/segment-wrappers.ts +291 -0
  205. package/src/router/telemetry-otel.ts +299 -0
  206. package/src/router/telemetry.ts +300 -0
  207. package/src/router/timeout.ts +148 -0
  208. package/src/router/trie-matching.ts +239 -0
  209. package/src/router/types.ts +78 -3
  210. package/src/router.ts +740 -4252
  211. package/src/rsc/handler-context.ts +45 -0
  212. package/src/rsc/handler.ts +907 -797
  213. package/src/rsc/helpers.ts +140 -6
  214. package/src/rsc/index.ts +0 -20
  215. package/src/rsc/loader-fetch.ts +229 -0
  216. package/src/rsc/manifest-init.ts +90 -0
  217. package/src/rsc/nonce.ts +14 -0
  218. package/src/rsc/origin-guard.ts +141 -0
  219. package/src/rsc/progressive-enhancement.ts +393 -0
  220. package/src/rsc/response-error.ts +37 -0
  221. package/src/rsc/response-route-handler.ts +347 -0
  222. package/src/rsc/rsc-rendering.ts +246 -0
  223. package/src/rsc/runtime-warnings.ts +42 -0
  224. package/src/rsc/server-action.ts +358 -0
  225. package/src/rsc/ssr-setup.ts +128 -0
  226. package/src/rsc/types.ts +46 -11
  227. package/src/search-params.ts +230 -0
  228. package/src/segment-content-promise.ts +67 -0
  229. package/src/segment-loader-promise.ts +122 -0
  230. package/src/segment-system.tsx +134 -36
  231. package/src/server/context.ts +341 -61
  232. package/src/server/cookie-store.ts +190 -0
  233. package/src/server/fetchable-loader-store.ts +37 -0
  234. package/src/server/handle-store.ts +113 -15
  235. package/src/server/loader-registry.ts +24 -64
  236. package/src/server/request-context.ts +607 -81
  237. package/src/server.ts +35 -130
  238. package/src/ssr/index.tsx +103 -30
  239. package/src/static-handler.ts +126 -0
  240. package/src/theme/ThemeProvider.tsx +21 -15
  241. package/src/theme/ThemeScript.tsx +5 -5
  242. package/src/theme/constants.ts +5 -2
  243. package/src/theme/index.ts +4 -14
  244. package/src/theme/theme-context.ts +4 -30
  245. package/src/theme/theme-script.ts +21 -18
  246. package/src/types/boundaries.ts +158 -0
  247. package/src/types/cache-types.ts +198 -0
  248. package/src/types/error-types.ts +192 -0
  249. package/src/types/global-namespace.ts +100 -0
  250. package/src/types/handler-context.ts +791 -0
  251. package/src/types/index.ts +88 -0
  252. package/src/types/loader-types.ts +210 -0
  253. package/src/types/route-config.ts +170 -0
  254. package/src/types/route-entry.ts +120 -0
  255. package/src/types/segments.ts +150 -0
  256. package/src/types.ts +1 -1623
  257. package/src/urls/include-helper.ts +207 -0
  258. package/src/urls/index.ts +53 -0
  259. package/src/urls/path-helper-types.ts +372 -0
  260. package/src/urls/path-helper.ts +364 -0
  261. package/src/urls/pattern-types.ts +107 -0
  262. package/src/urls/response-types.ts +116 -0
  263. package/src/urls/type-extraction.ts +372 -0
  264. package/src/urls/urls-function.ts +98 -0
  265. package/src/urls.ts +1 -802
  266. package/src/use-loader.tsx +161 -81
  267. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  268. package/src/vite/discovery/discover-routers.ts +348 -0
  269. package/src/vite/discovery/prerender-collection.ts +439 -0
  270. package/src/vite/discovery/route-types-writer.ts +258 -0
  271. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  272. package/src/vite/discovery/state.ts +117 -0
  273. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  274. package/src/vite/index.ts +15 -1133
  275. package/src/vite/plugin-types.ts +103 -0
  276. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  277. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  278. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  279. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  280. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  281. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  282. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  283. package/src/vite/plugins/expose-id-utils.ts +299 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  290. package/src/vite/plugins/performance-tracks.ts +88 -0
  291. package/src/vite/plugins/refresh-cmd.ts +127 -0
  292. package/src/vite/plugins/use-cache-transform.ts +323 -0
  293. package/src/vite/plugins/version-injector.ts +83 -0
  294. package/src/vite/plugins/version-plugin.ts +266 -0
  295. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +462 -0
  298. package/src/vite/router-discovery.ts +977 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  304. package/src/vite/utils/prerender-utils.ts +221 -0
  305. package/src/vite/utils/shared-utils.ts +170 -0
  306. package/CLAUDE.md +0 -43
  307. package/src/browser/lru-cache.ts +0 -69
  308. package/src/browser/request-controller.ts +0 -164
  309. package/src/cache/memory-store.ts +0 -253
  310. package/src/href-context.ts +0 -33
  311. package/src/href.ts +0 -255
  312. package/src/server/route-manifest-cache.ts +0 -173
  313. package/src/vite/expose-handle-id.ts +0 -209
  314. package/src/vite/expose-loader-id.ts +0 -426
  315. package/src/vite/expose-location-state-id.ts +0 -177
  316. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,1121 @@
1
+ import type { ReactNode } from "react";
2
+ import type {
3
+ PartialCacheOptions,
4
+ Handler,
5
+ LoaderDefinition,
6
+ MiddlewareFn,
7
+ ShouldRevalidateFn,
8
+ TransitionConfig,
9
+ } from "../types.js";
10
+ import {
11
+ getContext,
12
+ getNamePrefix,
13
+ getUrlPrefix,
14
+ type EntryData,
15
+ type InterceptEntry,
16
+ } from "../server/context";
17
+ import { invariant } from "../errors";
18
+ import { isCachedFunction } from "../cache/taint.js";
19
+ import { RSCRouterContext } from "../server/context";
20
+ import { isStaticHandler } from "../static-handler.js";
21
+ import RootLayout from "../server/root-layout";
22
+ import type {
23
+ AllUseItems,
24
+ RouteItem,
25
+ ParallelItem,
26
+ InterceptItem,
27
+ MiddlewareItem,
28
+ RevalidateItem,
29
+ LoaderItem,
30
+ LoadingItem,
31
+ ErrorBoundaryItem,
32
+ NotFoundBoundaryItem,
33
+ LayoutItem,
34
+ WhenItem,
35
+ CacheItem,
36
+ TransitionItem,
37
+ UseItems,
38
+ } from "../route-types.js";
39
+ import type { RouteHelpers } from "./helpers-types.js";
40
+ import { resolveHandlerUse, mergeHandlerUse } from "./resolve-handler-use.js";
41
+
42
+ /**
43
+ * Check if an item contains routes (directly or inside nested structures like cache).
44
+ * Used to determine if a layout or cache should be treated as an orphan.
45
+ */
46
+ const hasRoutesInItem = (item: AllUseItems): boolean => {
47
+ if (item.type === "route") return true;
48
+ // Lazy includes contain deferred routes — treat them as having routes
49
+ // to prevent the parent layout from being misclassified as orphan,
50
+ // which would clear its parent pointer and break the middleware chain.
51
+ if (item.type === "include") return true;
52
+ if (item.type === "cache" && item.uses) {
53
+ return item.uses.some((child) => hasRoutesInItem(child));
54
+ }
55
+ if (item.type === "layout" && item.uses) {
56
+ return item.uses.some((child) => hasRoutesInItem(child));
57
+ }
58
+ if (item.type === "middleware" && item.uses) {
59
+ return item.uses.some((child) => hasRoutesInItem(child));
60
+ }
61
+ return false;
62
+ };
63
+
64
+ const revalidate: RouteHelpers<any, any>["revalidate"] = (fn) => {
65
+ const ctx = getContext().getStore();
66
+ if (!ctx) throw new Error("revalidate() must be called inside map()");
67
+
68
+ // Attach to last entry in stack
69
+ const parent = ctx.parent;
70
+ if (!parent || !("revalidate" in parent)) {
71
+ invariant(false, "No parent entry available for revalidate()");
72
+ }
73
+ const name = `$${getContext().getNextIndex("revalidate")}`;
74
+ parent.revalidate.push(fn);
75
+ return { name, type: "revalidate" } as RevalidateItem;
76
+ };
77
+
78
+ /**
79
+ * Error boundary helper - attaches an error fallback to the current entry
80
+ *
81
+ * When an error occurs during rendering of this segment or its children,
82
+ * the fallback will be rendered instead. The fallback can be:
83
+ * - A static ReactNode (e.g., <ErrorPage />)
84
+ * - A handler function that receives error info and reset function
85
+ *
86
+ * Error boundaries catch errors from:
87
+ * - Middleware execution
88
+ * - Loader execution
89
+ * - Handler/component rendering
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * layout(<ShopLayout />, () => [
94
+ * errorBoundary(<ShopErrorFallback />),
95
+ * route("products.detail", ProductDetail),
96
+ * ])
97
+ *
98
+ * // Or with handler for dynamic error UI:
99
+ * route("products.detail", ProductDetail, () => [
100
+ * errorBoundary(({ error, reset }) => (
101
+ * <div>
102
+ * <h2>Product failed to load</h2>
103
+ * <p>{error.message}</p>
104
+ * <button onClick={reset}>Retry</button>
105
+ * </div>
106
+ * )),
107
+ * ])
108
+ * ```
109
+ */
110
+ const errorBoundary: RouteHelpers<any, any>["errorBoundary"] = (fallback) => {
111
+ const ctx = getContext().getStore();
112
+ if (!ctx) throw new Error("errorBoundary() must be called inside map()");
113
+
114
+ // Attach to parent entry in stack
115
+ const parent = ctx.parent;
116
+ if (!parent || !("errorBoundary" in parent)) {
117
+ invariant(false, "No parent entry available for errorBoundary()");
118
+ }
119
+ const name = `$${getContext().getNextIndex("errorBoundary")}`;
120
+ parent.errorBoundary.push(fallback);
121
+ return { name, type: "errorBoundary" } as ErrorBoundaryItem;
122
+ };
123
+
124
+ /**
125
+ * NotFound boundary helper - attaches a not-found fallback to the current entry
126
+ *
127
+ * When a DataNotFoundError is thrown (via notFound()) during rendering of this
128
+ * segment or its children, the fallback will be rendered instead. The fallback can be:
129
+ * - A static ReactNode (e.g., <ProductNotFound />)
130
+ * - A handler function that receives not found info
131
+ *
132
+ * NotFound boundaries catch DataNotFoundError from:
133
+ * - Loader execution
134
+ * - Handler/component rendering
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * layout(<ShopLayout />, () => [
139
+ * notFoundBoundary(<ProductNotFound />),
140
+ * route("products.detail", ProductDetail),
141
+ * ])
142
+ *
143
+ * // Or with handler for dynamic not found UI:
144
+ * route("products.detail", ProductDetail, () => [
145
+ * notFoundBoundary(({ notFound }) => (
146
+ * <div>
147
+ * <h2>Product not found</h2>
148
+ * <p>{notFound.message}</p>
149
+ * <a href="/products">Browse all products</a>
150
+ * </div>
151
+ * )),
152
+ * ])
153
+ * ```
154
+ */
155
+ const notFoundBoundary: RouteHelpers<any, any>["notFoundBoundary"] = (
156
+ fallback,
157
+ ) => {
158
+ const ctx = getContext().getStore();
159
+ if (!ctx) throw new Error("notFoundBoundary() must be called inside map()");
160
+
161
+ // Attach to parent entry in stack
162
+ const parent = ctx.parent;
163
+ if (!parent || !("notFoundBoundary" in parent)) {
164
+ invariant(false, "No parent entry available for notFoundBoundary()");
165
+ }
166
+ const name = `$${getContext().getNextIndex("notFoundBoundary")}`;
167
+ parent.notFoundBoundary.push(fallback);
168
+ return { name, type: "notFoundBoundary" } as NotFoundBoundaryItem;
169
+ };
170
+
171
+ /**
172
+ * When helper - defines a condition for intercept activation
173
+ *
174
+ * Only valid inside intercept() use() callback. The when() function
175
+ * is captured by the intercept and stored in its `when` array.
176
+ * During soft navigation, all when() conditions must return true
177
+ * for the intercept to activate.
178
+ */
179
+ const when: RouteHelpers<any, any>["when"] = (fn) => {
180
+ const ctx = getContext().getStore();
181
+ if (!ctx) throw new Error("when() must be called inside intercept()");
182
+
183
+ // The when() function needs to be captured by the intercept's tempParent
184
+ // which should have a `when` array. If not present, we're not inside intercept()
185
+ const parent = ctx.parent as any;
186
+ if (!parent || !("when" in parent)) {
187
+ invariant(
188
+ false,
189
+ "when() can only be used inside intercept() use() callback",
190
+ );
191
+ }
192
+
193
+ const name = `$${getContext().getNextIndex("when")}`;
194
+ parent.when.push(fn);
195
+ return { name, type: "when" } as WhenItem;
196
+ };
197
+
198
+ /**
199
+ * Cache helper - defines caching configuration for segments
200
+ *
201
+ * Creates a cache boundary that applies to all children unless overridden.
202
+ * When used without children, attaches cache config to the parent entry
203
+ * (e.g., for loader-specific caching).
204
+ *
205
+ * Supports these call signatures:
206
+ * - cache() - no args, uses app-level defaults (for loader caching)
207
+ * - cache(() => [...]) - wraps children with app-level defaults
208
+ * - cache('profileName') - uses a named cache profile
209
+ * - cache('profileName', () => [...]) - named profile with children
210
+ * - cache({ ttl: 60 }, () => [...]) - with explicit options
211
+ */
212
+ const cache: RouteHelpers<any, any>["cache"] = (
213
+ optionsOrChildren?:
214
+ | PartialCacheOptions
215
+ | false
216
+ | string
217
+ | (() => UseItems<AllUseItems>),
218
+ maybeChildren?: () => UseItems<AllUseItems>,
219
+ ) => {
220
+ const store = getContext();
221
+ const ctx = store.getStore();
222
+ if (!ctx) throw new Error("cache() must be called inside map()");
223
+
224
+ // Handle overloaded signature
225
+ let options: PartialCacheOptions | false;
226
+ let children: (() => UseItems<AllUseItems>) | undefined;
227
+
228
+ if (optionsOrChildren === undefined) {
229
+ // cache() - no args, use defaults
230
+ options = {};
231
+ 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
+ } else if (typeof optionsOrChildren === "function") {
245
+ // cache(() => [...]) - use empty options (will use defaults)
246
+ options = {};
247
+ children = optionsOrChildren;
248
+ } else {
249
+ // cache(options, children) - explicit options
250
+ options = optionsOrChildren;
251
+ children = maybeChildren;
252
+ }
253
+
254
+ // Allocate a single index for this cache() call (used in all paths)
255
+ const cacheIndex = store.getNextIndex("cache");
256
+ const name = `$${cacheIndex}`;
257
+ const cacheConfig = { options };
258
+
259
+ // If no children, create an orphan cache entry (like orphan layouts)
260
+ // This allows cache() to wrap subsequent siblings
261
+ if (!children) {
262
+ const parent = ctx.parent as any;
263
+
264
+ // Check if we're inside a loader() use() callback - special case for loader caching
265
+ if (parent && parent.type === "loader") {
266
+ // Direct assignment to loader entry's cache field
267
+ parent.cache = cacheConfig;
268
+ return { name, type: "cache" } as CacheItem;
269
+ }
270
+
271
+ // Create orphan cache entry (like orphan layout)
272
+ // Subsequent siblings in the same array will attach to this entry
273
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
274
+ const cacheUrlPrefix = getUrlPrefix();
275
+
276
+ const entry = {
277
+ id: namespace,
278
+ shortCode: store.getShortCode("cache"),
279
+ type: "cache",
280
+ parent: parent, // link to current parent for hierarchy
281
+ cache: cacheConfig,
282
+ 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;
294
+
295
+ // Attach to parent's layout array (cache entries are structural like layouts)
296
+ if (parent && "layout" in parent) {
297
+ parent.layout.push(entry);
298
+ }
299
+
300
+ // Update context parent so subsequent siblings attach to this cache entry
301
+ // This makes cache() act as sugar for cache(() => [...])
302
+ ctx.parent = entry;
303
+
304
+ return { name: namespace, type: "cache" } as CacheItem;
305
+ }
306
+
307
+ // With children: create a cache entry (like layout with caching semantics)
308
+ const namespace = `${ctx.namespace}.${cacheIndex}`;
309
+ const cacheShortCode = store.getShortCode("cache");
310
+
311
+ const cacheUrlPrefix2 = getUrlPrefix();
312
+
313
+ const entry = {
314
+ id: namespace,
315
+ shortCode: cacheShortCode,
316
+ type: "cache",
317
+ parent: ctx.parent,
318
+ cache: cacheConfig,
319
+ // Cache entries render like layouts (with Outlet as default handler)
320
+ 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;
332
+
333
+ // 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}]`,
339
+ );
340
+
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
+ }
355
+
356
+ return { name: namespace, type: "cache", uses: result } as CacheItem;
357
+ };
358
+
359
+ const middleware: RouteHelpers<any, any>["middleware"] = (...args: any[]) => {
360
+ // Four call forms:
361
+ // middleware(fn) — single fn, sibling
362
+ // middleware(fn, () => [...]) — single fn, wrapping
363
+ // middleware([fn1, fn2]) — array, sibling
364
+ // middleware([fn1, fn2], () => [...]) — array, wrapping
365
+ const isArray = Array.isArray(args[0]);
366
+
367
+ // Reject the removed variadic form before executing anything.
368
+ // middleware(fn1, fn2, fn3) — 3+ args, always wrong.
369
+ // middleware(fn1, fn2) where fn2 is a middleware fn (length >= 1), not a
370
+ // children callback (length === 0) — legacy two-fn form, reject early.
371
+ if (
372
+ args.length > 2 ||
373
+ (!isArray &&
374
+ args.length === 2 &&
375
+ typeof args[1] === "function" &&
376
+ args[1].length > 0)
377
+ ) {
378
+ throw new Error(
379
+ "middleware() no longer accepts variadic arguments. " +
380
+ "Use middleware([fn1, fn2, ...]) instead of middleware(fn1, fn2, ...).",
381
+ );
382
+ }
383
+
384
+ const fns: MiddlewareFn<any>[] = isArray ? args[0] : [args[0]];
385
+ const children: (() => any[]) | undefined =
386
+ typeof args[1] === "function" ? args[1] : undefined;
387
+
388
+ // Prevent "use cache" functions from being used as middleware.
389
+ for (const f of fns) {
390
+ if (isCachedFunction(f)) {
391
+ throw new Error(
392
+ `A "use cache" function cannot be used as middleware. ` +
393
+ `Cached functions return data and do not participate in the ` +
394
+ `middleware chain. Remove the "use cache" directive or use a ` +
395
+ `regular middleware function instead.`,
396
+ );
397
+ }
398
+ }
399
+
400
+ const store = getContext();
401
+ const ctx = store.getStore();
402
+ if (!ctx) throw new Error("middleware() must be called inside map()");
403
+
404
+ if (!children) {
405
+ // Sibling mode: attach to parent entry
406
+ const parent = ctx.parent;
407
+ if (!parent || !("middleware" in parent)) {
408
+ invariant(false, "No parent entry available for middleware()");
409
+ }
410
+ const name = `$${store.getNextIndex("middleware")}`;
411
+ parent.middleware.push(...fns);
412
+ return { name, type: "middleware" } as MiddlewareItem;
413
+ }
414
+
415
+ // Wrapping mode: create a transparent layout that carries the middleware
416
+ const mwIndex = store.getNextIndex("middleware");
417
+ const namespace = `${ctx.namespace}.${mwIndex}`;
418
+
419
+ const urlPrefix = getUrlPrefix();
420
+ const entry = {
421
+ id: namespace,
422
+ shortCode: store.getShortCode("layout"),
423
+ type: "layout",
424
+ parent: ctx.parent,
425
+ handler: RootLayout,
426
+ loading: undefined,
427
+ middleware: [...fns],
428
+ revalidate: [],
429
+ errorBoundary: [],
430
+ notFoundBoundary: [],
431
+ layout: [],
432
+ parallel: {},
433
+ intercept: [],
434
+ loader: [],
435
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
436
+ } as EntryData;
437
+
438
+ // Run children callback. If the second arg was actually a middleware fn
439
+ // (old variadic form: middleware(mw1, mw2)), this will return a non-array
440
+ // and the invariant below gives a clear migration error.
441
+ const rawResult = store.run(namespace, entry, children);
442
+
443
+ invariant(
444
+ Array.isArray(rawResult),
445
+ "middleware(fn, children) expects the second argument to return an array of use items. " +
446
+ "To pass multiple middleware, use middleware([fn1, fn2]).",
447
+ );
448
+
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}]`,
454
+ );
455
+
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
+ }
468
+
469
+ return {
470
+ name: namespace,
471
+ type: "middleware",
472
+ uses: result,
473
+ } as MiddlewareItem;
474
+ };
475
+
476
+ 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()");
480
+
481
+ if (!ctx.parent || !ctx.parent?.parallel) {
482
+ invariant(false, "No parent entry available for parallel()");
483
+ }
484
+
485
+ invariant(
486
+ ctx.parent.type !== "parallel",
487
+ "parallel() cannot be nested inside another parallel()",
488
+ );
489
+
490
+ const slotNames = Object.keys(slots as Record<string, any>) as `@${string}`[];
491
+
492
+ const namespace = `${ctx.namespace}.$${store.getNextIndex("parallel")}`;
493
+
494
+ // Unwrap slot values. A slot value can be:
495
+ // - a Handler / ReactNode (legacy form)
496
+ // - a Static() definition (build-time only)
497
+ // - a slot descriptor `{ handler, use? }` for slot-local overrides
498
+ // The descriptor's `use` runs after the broadcast `use` for that slot,
499
+ // so single-assignment items like `loading()` placed there win without
500
+ // affecting siblings.
501
+ const unwrappedSlots: Record<string, any> = {};
502
+ const slotLocalUses: Record<string, (() => any[]) | undefined> = {};
503
+ let hasStaticSlot = false;
504
+ const staticSlotIds: Record<string, string> = {};
505
+ for (const [slotName, rawSlot] of Object.entries(
506
+ slots as Record<string, any>,
507
+ )) {
508
+ let slotHandler: any = rawSlot;
509
+ if (isSlotDescriptor(rawSlot)) {
510
+ slotHandler = rawSlot.handler;
511
+ slotLocalUses[slotName] = rawSlot.use;
512
+ }
513
+ if (isStaticHandler(slotHandler)) {
514
+ hasStaticSlot = true;
515
+ unwrappedSlots[slotName] = slotHandler.handler;
516
+ if (slotHandler.$$id) {
517
+ staticSlotIds[slotName] = slotHandler.$$id;
518
+ // Capture namespace prefix for build-time reverse() resolution
519
+ if (ctx.namePrefix) {
520
+ (slotHandler as any).$$routePrefix = ctx.namePrefix;
521
+ }
522
+ }
523
+ } else {
524
+ unwrappedSlots[slotName] = slotHandler;
525
+ }
526
+ }
527
+
528
+ // Create full EntryData for parallel with its own loaders/revalidate/loading
529
+ const parallelUrlPrefix = getUrlPrefix();
530
+ const entry = {
531
+ id: namespace,
532
+ shortCode: store.getShortCode("parallel"),
533
+ type: "parallel",
534
+ parent: null, // Parallels don't participate in parent chain traversal
535
+ 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
+ ...(parallelUrlPrefix ? { mountPath: parallelUrlPrefix } : {}),
546
+ ...(hasStaticSlot
547
+ ? {
548
+ isStaticPrerender: true as const,
549
+ ...(Object.keys(staticSlotIds).length > 0
550
+ ? { staticHandlerIds: staticSlotIds }
551
+ : {}),
552
+ }
553
+ : {}),
554
+ } satisfies EntryData;
555
+
556
+ for (const slotName of slotNames) {
557
+ const slotEntry = {
558
+ ...entry,
559
+ handler: { [slotName]: unwrappedSlots[slotName]! },
560
+ middleware: [...entry.middleware],
561
+ revalidate: [...entry.revalidate],
562
+ errorBoundary: [...entry.errorBoundary],
563
+ notFoundBoundary: [...entry.notFoundBoundary],
564
+ layout: [...entry.layout],
565
+ parallel: { ...entry.parallel },
566
+ intercept: [...entry.intercept],
567
+ loader: [...entry.loader],
568
+ ...(entry.staticHandlerIds?.[slotName]
569
+ ? {
570
+ isStaticPrerender: true as const,
571
+ staticHandlerIds: { [slotName]: entry.staticHandlerIds[slotName]! },
572
+ }
573
+ : {
574
+ isStaticPrerender: undefined,
575
+ staticHandlerIds: undefined,
576
+ }),
577
+ } satisfies EntryData;
578
+
579
+ // Per-slot merge order (narrowest-scope-wins for single-assignment items
580
+ // like loading()):
581
+ // 1. handler.use — defaults baked into the handler
582
+ // 2. shared `use` — broadcast at the parallel() call site
583
+ // 3. slot-local `use` — per-slot override via `{ handler, use }` descriptor
584
+ // Items that accumulate (loader, middleware, revalidate, …) compose
585
+ // across all three layers regardless of order.
586
+ const rawSlot = (slots as Record<string, any>)[slotName];
587
+ const slotHandlerForUse = isSlotDescriptor(rawSlot)
588
+ ? rawSlot.handler
589
+ : rawSlot;
590
+ const slotHandlerUse = resolveHandlerUse(slotHandlerForUse);
591
+ const slotLocalUse = slotLocalUses[slotName];
592
+ const explicitUse = combineExplicitUses(use, slotLocalUse);
593
+ const slotMergedUse = mergeHandlerUse(
594
+ slotHandlerUse,
595
+ explicitUse,
596
+ "parallel",
597
+ );
598
+ 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}]`,
603
+ );
604
+ }
605
+
606
+ ctx.parent.parallel[slotName] = slotEntry;
607
+ }
608
+ return { name: namespace, type: "parallel" } as ParallelItem;
609
+ };
610
+
611
+ function isSlotDescriptor(
612
+ value: unknown,
613
+ ): value is { handler: unknown; use?: () => any[] } {
614
+ return (
615
+ typeof value === "object" &&
616
+ value !== null &&
617
+ !("__brand" in value) &&
618
+ "handler" in value &&
619
+ typeof (value as any).handler !== "undefined"
620
+ );
621
+ }
622
+
623
+ function combineExplicitUses(
624
+ sharedUse: (() => any[]) | undefined,
625
+ slotLocalUse: (() => any[]) | undefined,
626
+ ): (() => any[]) | undefined {
627
+ if (!sharedUse && !slotLocalUse) return undefined;
628
+ if (!slotLocalUse) return sharedUse;
629
+ if (!sharedUse) return slotLocalUse;
630
+ return () => [...sharedUse(), ...slotLocalUse()];
631
+ }
632
+
633
+ /**
634
+ * Intercept helper - defines an intercepting route for soft navigation
635
+ */
636
+ const intercept = (
637
+ slotName: `@${string}`,
638
+ routeName: string,
639
+ handler: any,
640
+ use?: () => any[],
641
+ ) => {
642
+ const store = getContext();
643
+ const ctx = store.getStore();
644
+ if (!ctx) throw new Error("intercept() must be called inside map()");
645
+
646
+ if (!ctx.parent || !ctx.parent?.intercept) {
647
+ invariant(false, "No parent entry available for intercept()");
648
+ }
649
+
650
+ invariant(
651
+ ctx.parent.type !== "parallel",
652
+ "intercept() cannot be used inside parallel()",
653
+ );
654
+
655
+ const namespace = `${ctx.namespace}.$${store.getNextIndex("intercept")}.${slotName}`;
656
+
657
+ // Dot-prefixed = local (add include prefix), unprefixed = global (use as-is)
658
+ const isLocal = typeof routeName === "string" && routeName.startsWith(".");
659
+ const bareRouteName = isLocal ? routeName.slice(1) : routeName;
660
+ const namePrefix = getNamePrefix();
661
+ const prefixedRouteName =
662
+ isLocal && namePrefix ? `${namePrefix}.${bareRouteName}` : bareRouteName;
663
+
664
+ // Create intercept entry with its own loaders/revalidate/middleware/when
665
+ const entry: InterceptEntry = {
666
+ slotName: slotName as `@${string}`,
667
+ routeName: prefixedRouteName,
668
+ handler,
669
+ middleware: [],
670
+ revalidate: [],
671
+ errorBoundary: [],
672
+ notFoundBoundary: [],
673
+ loader: [],
674
+ when: [], // Selector conditions for conditional interception
675
+ };
676
+
677
+ // Merge handler.use defaults with explicit use
678
+ const handlerUseFn = resolveHandlerUse(handler);
679
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "intercept");
680
+
681
+ // Run merged use callback to collect loaders, revalidate, middleware, etc.
682
+ 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
688
+ const capturedLayouts: EntryData[] = [];
689
+
690
+ const tempParent = {
691
+ ...originalParent,
692
+ middleware: entry.middleware,
693
+ revalidate: entry.revalidate,
694
+ errorBoundary: entry.errorBoundary,
695
+ notFoundBoundary: entry.notFoundBoundary,
696
+ loader: entry.loader,
697
+ layout: capturedLayouts, // Capture layout() calls
698
+ when: entry.when, // Capture when() conditions
699
+ // Use getter/setter to capture loading on the entry
700
+ get loading() {
701
+ return entry.loading;
702
+ },
703
+ set loading(value: ReactNode | false | undefined) {
704
+ entry.loading = value;
705
+ },
706
+ };
707
+ ctx.parent = tempParent as EntryData;
708
+
709
+ const result = mergedUse()?.flat(3);
710
+
711
+ // Restore original parent
712
+ ctx.parent = originalParent;
713
+
714
+ // Extract layout from captured layouts (use first one if multiple)
715
+ // Layout inside intercept should always be ReactNode or Handler, not Record slots
716
+ if (capturedLayouts.length > 0 && capturedLayouts[0].type === "layout") {
717
+ entry.layout = capturedLayouts[0].handler as
718
+ | ReactNode
719
+ | Handler<any, any, any>;
720
+ }
721
+
722
+ invariant(
723
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
724
+ `intercept() use() callback must return an array of use items [${namespace}]`,
725
+ );
726
+ }
727
+
728
+ ctx.parent.intercept.push(entry);
729
+ return { name: namespace, type: "intercept" } as InterceptItem;
730
+ };
731
+
732
+ /**
733
+ * Loader helper - attaches a loader to the current entry
734
+ */
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()");
739
+
740
+ // Attach to last entry in stack
741
+ if (!ctx.parent || !ctx.parent?.loader) {
742
+ invariant(false, "No parent entry available for loader()");
743
+ }
744
+
745
+ const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
746
+
747
+ // Create loader entry with empty revalidate array
748
+ const loaderEntry = {
749
+ loader: loaderDef,
750
+ revalidate: [] as ShouldRevalidateFn<any, any>[],
751
+ };
752
+
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;
757
+ // Create a temporary "parent" with type "loader" so cache() can detect it.
758
+ // Save existing .cache to distinguish inherited config from newly set config.
759
+ const parentCache = (originalParent as any).cache;
760
+ const tempParent = {
761
+ ...originalParent,
762
+ type: "loader",
763
+ revalidate: loaderEntry.revalidate,
764
+ };
765
+ ctx.parent = tempParent as EntryData;
766
+
767
+ const result = use()?.flat(3);
768
+
769
+ // 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.
772
+ if (
773
+ (tempParent as any).cache &&
774
+ (tempParent as any).cache !== parentCache
775
+ ) {
776
+ (loaderEntry as any).cache = (tempParent as any).cache;
777
+ }
778
+
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
+ );
786
+ }
787
+
788
+ ctx.parent.loader.push(loaderEntry);
789
+ return { name, type: "loader" } as LoaderItem;
790
+ };
791
+
792
+ /**
793
+ * Loading helper - attaches a loading component to the current entry
794
+ * Loading components are static (no context) and shown during navigation
795
+ */
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()");
800
+
801
+ const parent = ctx.parent;
802
+ if (!parent || !("loading" in parent)) {
803
+ invariant(false, "No parent entry available for loading()");
804
+ }
805
+
806
+ // Unwrap function form: loading(() => <Skeleton />) → loading(<Skeleton />)
807
+ const resolved =
808
+ typeof component === "function" ? (component as () => any)() : component;
809
+
810
+ // If ssr: false and we're in SSR, set loading to false
811
+ if (options?.ssr === false && ctx.isSSR) {
812
+ parent.loading = false;
813
+ } else {
814
+ parent.loading = resolved;
815
+ }
816
+
817
+ const name = `$${store.getNextIndex("loading")}`;
818
+ return { name, type: "loading" } as LoadingItem;
819
+ };
820
+
821
+ /**
822
+ * Transition helper - attaches a ViewTransition config to the current entry
823
+ * or wraps a group of routes in a transparent layout with ViewTransition
824
+ */
825
+ const transitionFn = (
826
+ configOrChildren?: TransitionConfig | (() => UseItems<AllUseItems>),
827
+ maybeChildren?: () => UseItems<AllUseItems>,
828
+ ): TransitionItem => {
829
+ // Resolve overloaded arguments:
830
+ // transition() -> config={}, children=undefined
831
+ // transition(config) -> config=config, children=undefined
832
+ // transition(children) -> config={}, children=children
833
+ // transition(config, children) -> config=config, children=children
834
+ const config: TransitionConfig =
835
+ typeof configOrChildren === "function" ? {} : (configOrChildren ?? {});
836
+ const children: (() => UseItems<AllUseItems>) | undefined =
837
+ typeof configOrChildren === "function" ? configOrChildren : maybeChildren;
838
+
839
+ const store = getContext();
840
+ const ctx = store.getStore();
841
+ if (!ctx) throw new Error("transition() must be called inside map()");
842
+
843
+ const name = `$${store.getNextIndex("transition")}`;
844
+
845
+ if (!children) {
846
+ // Position 1: child of path() — attach to parent entry
847
+ const parent = ctx.parent;
848
+ if (!parent || !("loading" in parent)) {
849
+ invariant(false, "No parent entry available for transition()");
850
+ }
851
+ parent.transition = config;
852
+ return { name, type: "transition" } as TransitionItem;
853
+ }
854
+
855
+ // Position 2: wrapper — create a transparent layout with transition config
856
+ const namespace = `${ctx.namespace}.${store.getNextIndex("transition")}`;
857
+ const entry = {
858
+ id: namespace,
859
+ shortCode: store.getShortCode("layout"),
860
+ type: "layout",
861
+ parent: ctx.parent,
862
+ handler: RootLayout,
863
+ loading: undefined,
864
+ 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);
876
+
877
+ invariant(
878
+ Array.isArray(result) && result.every((item) => isValidUseItem(item)),
879
+ `transition() children callback must return an array of use items [${namespace}]`,
880
+ );
881
+
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
+ }
894
+
895
+ return { name: namespace, type: "transition" } as TransitionItem;
896
+ };
897
+
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()");
902
+
903
+ const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${name}`;
904
+
905
+ const entry = {
906
+ id: namespace,
907
+ shortCode: store.getShortCode("route"),
908
+ type: "route",
909
+ parent: ctx.parent,
910
+ 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
+ } satisfies EntryData;
921
+
922
+ /* We will throw if user is registring same route name twice */
923
+ invariant(
924
+ ctx.manifest.get(name) === undefined,
925
+ `Duplicate route name: ${name} at ${namespace}`,
926
+ );
927
+ /* Register route entry */
928
+ ctx.manifest.set(name, entry);
929
+ /* Merge handler.use defaults with explicit use */
930
+ const handlerUseFn = resolveHandlerUse(handler);
931
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "route");
932
+ /* Run use and attach handlers */
933
+ 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}]`,
938
+ );
939
+ return { name: namespace, type: "route", uses: result } as RouteItem;
940
+ }
941
+
942
+ /* typesafe item */
943
+ return { name: namespace, type: "route" } as RouteItem;
944
+ };
945
+
946
+ 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()");
950
+
951
+ invariant(
952
+ !ctx.parent || ctx.parent.type !== "parallel",
953
+ "layout() cannot be used inside parallel()",
954
+ );
955
+
956
+ const isRoot = !ctx.parent || ctx.parent === null;
957
+ const nextIndex = isRoot ? "$root" : store.getNextIndex("layout");
958
+ const namespace = `${ctx.namespace}.${nextIndex}`;
959
+ const shortCode = store.getShortCode("layout");
960
+
961
+ // Unwrap static handler definition, extract the actual handler function
962
+ const isStatic = isStaticHandler(handler);
963
+ const unwrappedHandler = isStatic ? handler.handler : handler;
964
+
965
+ const urlPrefix = getUrlPrefix();
966
+ const entry = {
967
+ id: namespace,
968
+ shortCode,
969
+ type: "layout",
970
+ parent: ctx.parent,
971
+ 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
+ ...(urlPrefix ? { mountPath: urlPrefix } : {}),
982
+ ...(isStatic
983
+ ? {
984
+ isStaticPrerender: true as const,
985
+ ...(handler.$$id ? { staticHandlerId: handler.$$id } : {}),
986
+ }
987
+ : {}),
988
+ } satisfies EntryData;
989
+
990
+ // Capture namespace prefix on static handler for build-time reverse() resolution
991
+ if (isStatic && handler.$$id && ctx.namePrefix) {
992
+ (handler as any).$$routePrefix = ctx.namePrefix;
993
+ }
994
+
995
+ // Merge handler.use defaults with explicit use
996
+ const handlerUseFn = resolveHandlerUse(handler);
997
+ const mergedUse = mergeHandlerUse(handlerUseFn, use, "layout");
998
+
999
+ // Run merged use callback if present
1000
+ let result: AllUseItems[] | undefined;
1001
+ 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}]`,
1007
+ );
1008
+ }
1009
+
1010
+ // Check if this is an orphan layout (no routes in children, including nested caches)
1011
+ const hasRoutes =
1012
+ result &&
1013
+ Array.isArray(result) &&
1014
+ result.some((item) => hasRoutesInItem(item));
1015
+
1016
+ if (!hasRoutes) {
1017
+ // Orphan layouts must not contain other layouts as children.
1018
+ // If we're here, all child layouts are also orphan (if any had routes,
1019
+ // hasRoutesInItem would have returned true). Nested orphan chains are
1020
+ // confusing — use sibling orphan layouts instead.
1021
+ if (result) {
1022
+ invariant(
1023
+ !result.some((item) => item?.type === "layout"),
1024
+ `orphan layout cannot contain other layouts as children [${namespace}]`,
1025
+ );
1026
+ }
1027
+
1028
+ const parent = ctx.parent;
1029
+
1030
+ // Allow orphan layouts at root level if they're part of map() builder result
1031
+ if (!parent || parent === null) {
1032
+ if (!isRoot) {
1033
+ invariant(
1034
+ false,
1035
+ `Orphan layout cannot be used at non-root level without parent [${namespace}]`,
1036
+ );
1037
+ }
1038
+ // Root-level orphan is allowed (e.g., sibling layouts in map() builder)
1039
+ } else {
1040
+ // Has parent - register as orphan layout
1041
+ invariant(
1042
+ parent.type === "route" ||
1043
+ parent.type === "layout" ||
1044
+ parent.type === "cache",
1045
+ `Orphan layouts can only be defined inside route or layout > check [${namespace}]`,
1046
+ );
1047
+
1048
+ // Clear parent pointer for orphan layouts to prevent duplicate processing
1049
+ entry.parent = null;
1050
+ parent.layout.push(entry);
1051
+ }
1052
+ }
1053
+
1054
+ if (result) {
1055
+ return { name: namespace, type: "layout", uses: result } as LayoutItem;
1056
+ }
1057
+ return {
1058
+ name: namespace,
1059
+ type: "layout",
1060
+ } as LayoutItem;
1061
+ };
1062
+
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
+ };
1088
+
1089
+ // Global helper exports for direct import from @rangojs/router
1090
+ export {
1091
+ layout,
1092
+ cache,
1093
+ middleware,
1094
+ revalidate,
1095
+ parallel,
1096
+ intercept,
1097
+ when,
1098
+ errorBoundary,
1099
+ 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,
1119
+ isValidUseItem,
1120
+ isOrphanLayout,
1121
+ };