@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
@@ -1,7 +1,17 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { ReactNode } from "react";
3
- import type { PartialCacheOptions, ErrorBoundaryHandler, Handler, LoaderDefinition, MiddlewareFn, NotFoundBoundaryHandler, ShouldRevalidateFn } from "../types";
3
+ import type {
4
+ PartialCacheOptions,
5
+ ErrorBoundaryHandler,
6
+ Handler,
7
+ LoaderDefinition,
8
+ MiddlewareFn,
9
+ NotFoundBoundaryHandler,
10
+ ShouldRevalidateFn,
11
+ TransitionConfig,
12
+ } from "../types";
4
13
  import { invariant } from "../errors";
14
+ import type { DefaultRouteName } from "../types/global-namespace.js";
5
15
 
6
16
  // ============================================================================
7
17
  // Performance Metrics Types
@@ -13,9 +23,10 @@ import { invariant } from "../errors";
13
23
  * @internal This type is an implementation detail and may change without notice.
14
24
  */
15
25
  export interface PerformanceMetric {
16
- label: string; // e.g., "route-matching", "loader:UserLoader"
17
- duration: number; // milliseconds
18
- startTime: number; // relative to request start
26
+ label: string; // e.g., "route-matching", "loader:UserLoader"
27
+ duration: number; // milliseconds
28
+ startTime: number; // relative to request start
29
+ depth?: number; // nesting level for hierarchical display (0 = top-level)
19
30
  }
20
31
 
21
32
  /**
@@ -105,12 +116,14 @@ export type InterceptSegmentsState = {
105
116
  * @internal This type is an implementation detail and may change without notice.
106
117
  */
107
118
  export type InterceptSelectorContext<TEnv = any> = {
108
- from: URL; // Source URL (where user is coming from)
109
- to: URL; // Destination URL (where user is navigating to)
110
- params: Record<string, string>; // Matched route params
111
- request: Request; // The HTTP request object
112
- env: TEnv; // Platform bindings (Cloudflare env, etc.)
113
- segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
119
+ from: URL; // Source URL (where user is coming from)
120
+ to: URL; // Destination URL (where user is navigating to)
121
+ params: Record<string, string>; // Matched route params
122
+ request: Request; // The HTTP request object
123
+ env: TEnv; // Platform bindings (Cloudflare env, etc.)
124
+ segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
125
+ fromRouteName?: DefaultRouteName; // Named route being navigated away from (undefined for unnamed routes)
126
+ toRouteName?: DefaultRouteName; // Named route being navigated to (undefined for unnamed routes)
114
127
  };
115
128
 
116
129
  /**
@@ -119,7 +132,9 @@ export type InterceptSelectorContext<TEnv = any> = {
119
132
  *
120
133
  * @internal This type is an implementation detail and may change without notice.
121
134
  */
122
- export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>) => boolean;
135
+ export type InterceptWhenFn<TEnv = any> = (
136
+ ctx: InterceptSelectorContext<TEnv>,
137
+ ) => boolean;
123
138
 
124
139
  /**
125
140
  * Intercept entry stored in EntryData
@@ -128,55 +143,88 @@ export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>)
128
143
  * @internal This type is an implementation detail and may change without notice.
129
144
  */
130
145
  export type InterceptEntry = {
131
- slotName: `@${string}`; // e.g., "@modal"
132
- routeName: string; // e.g., "card"
133
- handler: ReactNode | Handler<any, any>;
146
+ slotName: `@${string}`; // e.g., "@modal"
147
+ routeName: string; // e.g., "card"
148
+ handler: ReactNode | Handler<any, any, any>;
134
149
  middleware: MiddlewareFn<any, any>[];
135
150
  revalidate: ShouldRevalidateFn<any, any>[];
136
151
  errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
137
152
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
138
153
  loader: LoaderEntry[];
139
154
  loading?: ReactNode | false;
140
- layout?: ReactNode | Handler<any, any>; // Wrapper layout with <Outlet /> for content
141
- when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
155
+ transition?: TransitionConfig;
156
+ layout?: ReactNode | Handler<any, any, any>; // Wrapper layout with <Outlet /> for content
157
+ when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
142
158
  };
143
159
 
160
+ export interface ParallelEntryData
161
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
162
+ type: "parallel";
163
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
+ loading?: ReactNode | false;
165
+ transition?: TransitionConfig;
166
+ /** Set when any parallel slot is a Static definition */
167
+ isStaticPrerender?: true;
168
+ /** Per-slot static handler $$ids for build-time store lookup */
169
+ staticHandlerIds?: Record<string, string>;
170
+ }
171
+
172
+ export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
+
144
174
  export type EntryPropSegments = {
145
175
  loader: LoaderEntry[];
146
176
  layout: EntryData[];
147
- parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
177
+ parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
148
178
  intercept: InterceptEntry[]; // intercept definitions for soft navigation
149
179
  };
150
180
 
151
181
  export type EntryData =
152
182
  | ({
153
183
  type: "route";
154
- handler: Handler<any, any>;
184
+ handler: Handler<any, any, any>;
155
185
  loading?: ReactNode | false;
186
+ transition?: TransitionConfig;
156
187
  /** URL pattern for this route (used by path() in urls()) */
157
188
  pattern?: string;
189
+ /** Set when handler is a Prerender definition */
190
+ isPrerender?: true;
191
+ /** Original PrerenderHandlerDefinition (for build-time getParams access) */
192
+ prerenderDef?: {
193
+ getParams?: (ctx: any) => Promise<any[]> | any[];
194
+ options?: { concurrency?: number };
195
+ };
196
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
197
+ isPassthrough?: true;
198
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
199
+ liveHandler?: Handler<any, any, any>;
200
+ /** Set when handler is a Static definition (build-time only) */
201
+ isStaticPrerender?: true;
202
+ /** Static handler $$id for build-time store lookup */
203
+ staticHandlerId?: string;
204
+ /** Response type for non-RSC routes (json, text, image, any) */
205
+ responseType?: string;
158
206
  } & EntryPropCommon &
159
207
  EntryPropDatas &
160
208
  EntryPropSegments)
161
209
  | ({
162
210
  type: "layout";
163
- handler: ReactNode | Handler<any, any>;
164
- loading?: ReactNode | false;
165
- } & EntryPropCommon &
166
- EntryPropDatas &
167
- EntryPropSegments)
168
- | ({
169
- type: "parallel";
170
- handler: Record<`@${string}`, Handler<any, any> | ReactNode>;
211
+ handler: ReactNode | Handler<any, any, any>;
171
212
  loading?: ReactNode | false;
213
+ transition?: TransitionConfig;
214
+ /** Set when handler is a Static definition (build-time only) */
215
+ isStaticPrerender?: true;
216
+ /** Static handler $$id for build-time store lookup */
217
+ staticHandlerId?: string;
172
218
  } & EntryPropCommon &
173
219
  EntryPropDatas &
174
220
  EntryPropSegments)
221
+ | ParallelEntryData
175
222
  | ({
176
223
  type: "cache";
177
224
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
178
- handler: ReactNode | Handler<any, any>;
225
+ handler: ReactNode | Handler<any, any, any>;
179
226
  loading?: ReactNode | false;
227
+ transition?: TransitionConfig;
180
228
  } & EntryPropCommon &
181
229
  EntryPropDatas &
182
230
  EntryPropSegments);
@@ -211,17 +259,53 @@ interface HelperContext {
211
259
  patternsByPrefix?: Map<string, Map<string, string>>;
212
260
  /** Trailing slash config per route name */
213
261
  trailingSlash?: Map<string, "never" | "always" | "ignore">;
262
+ /** Search param schemas per route name */
263
+ searchSchemas?: Map<string, Record<string, string>>;
214
264
  /** URL prefix from include() - applied to all path() patterns */
215
265
  urlPrefix?: string;
216
266
  /** Name prefix from include() - applied to all named routes */
217
267
  namePrefix?: string;
268
+ /** True when this scope is at root level (no named include boundary above).
269
+ * Routes at root scope allow dot-local reverse to fall back to bare names. */
270
+ rootScoped?: boolean;
218
271
  /** Run helper for cleaner middleware code */
219
272
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
220
273
  /** Tracked includes for build-time manifest generation */
221
274
  trackedIncludes?: TrackedInclude[];
275
+ /** Cache profiles for DSL-time cache("profileName") resolution */
276
+ cacheProfiles?: Record<
277
+ string,
278
+ import("../cache/profile-registry.js").CacheProfile
279
+ >;
280
+ /** True when resolving handlers inside a cache() DSL boundary.
281
+ * Read by ctx.get() to guard non-cacheable variable reads. */
282
+ insideCacheScope?: boolean;
283
+ /**
284
+ * Include scope string applied to direct-descendant shortCodes.
285
+ *
286
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
287
+ * `I1` from its parent's include counter and stores the composed scope
288
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
289
+ * evaluates lazily, the store's `includeScope` is set from that context so
290
+ * every direct-descendant shortCode is generated as
291
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
292
+ * collisions with siblings declared outside the include.
293
+ *
294
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
295
+ * parallels / caches inside the include absorb the scope into their own
296
+ * shortCodes and their children start fresh.
297
+ */
298
+ includeScope?: string;
222
299
  }
223
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
224
- new AsyncLocalStorage<HelperContext>();
300
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
301
+ // module re-evaluation. Without this, Vite's RSC module runner may create
302
+ // a new instance when context.ts is re-evaluated, while other modules still
303
+ // hold references to the old instance — causing getStore() to return
304
+ // undefined even inside a run() callback.
305
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
306
+ export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
307
+ globalThis as any
308
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
225
309
 
226
310
  export const getContext = (): {
227
311
  context: AsyncLocalStorage<HelperContext>;
@@ -229,21 +313,21 @@ export const getContext = (): {
229
313
  getParent: () => EntryData | null;
230
314
  getOrCreateStore: (forRoute?: string) => HelperContext;
231
315
  getNextIndex: (
232
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
316
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
233
317
  ) => string;
234
318
  getShortCode: (
235
- type: "layout" | "parallel" | "route" | "loader" | "cache"
319
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
236
320
  ) => string;
237
321
  run: <T>(
238
322
  namespace: string,
239
323
  parent: EntryData | null,
240
- callback: (...args: any[]) => T
324
+ callback: (...args: any[]) => T,
241
325
  ) => T;
242
326
  runWithStore: <T>(
243
327
  store: HelperContext,
244
328
  namespace: string,
245
329
  parent: EntryData | null,
246
- callback: (...args: any[]) => T
330
+ callback: (...args: any[]) => T,
247
331
  ) => T;
248
332
  } => {
249
333
  const context = RSCRouterContext;
@@ -262,6 +346,7 @@ export const getContext = (): {
262
346
  patterns: new Map<string, string>(),
263
347
  patternsByPrefix: new Map<string, Map<string, string>>(),
264
348
  trailingSlash: new Map<string, "never" | "always" | "ignore">(),
349
+ searchSchemas: new Map<string, Record<string, string>>(),
265
350
  } satisfies HelperContext;
266
351
  }
267
352
  return store;
@@ -270,7 +355,7 @@ export const getContext = (): {
270
355
  const store = context.getStore();
271
356
  if (!store) {
272
357
  throw new Error(
273
- "RSC Router context store is not available. Make sure to run within RSC Router context."
358
+ "RSC Router context store is not available. Make sure to run within RSC Router context.",
274
359
  );
275
360
  }
276
361
  return store;
@@ -284,7 +369,7 @@ export const getContext = (): {
284
369
  return store.parent;
285
370
  },
286
371
  getNextIndex: (
287
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
372
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
288
373
  ) => {
289
374
  const store = context.getStore();
290
375
  invariant(store, "No context RSCRouterContext available");
@@ -293,35 +378,55 @@ export const getContext = (): {
293
378
  store.counters[type] = index + 1;
294
379
  return `$${type}.${index}`;
295
380
  },
296
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
381
+ getShortCode: (
382
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
383
+ ) => {
297
384
  const store = context.getStore();
298
385
  invariant(store, "No context RSCRouterContext available");
299
386
 
300
387
  const parent = store.parent;
301
- const prefix = type === "layout" ? "L" : type === "parallel" ? "P" : type === "loader" ? "D" : type === "cache" ? "C" : "R";
302
- const mountPrefix = store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
388
+ const prefix =
389
+ type === "layout"
390
+ ? "L"
391
+ : type === "parallel"
392
+ ? "P"
393
+ : type === "loader"
394
+ ? "D"
395
+ : type === "cache"
396
+ ? "C"
397
+ : "R";
398
+ const mountPrefix =
399
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
400
+
401
+ const includeScope = store.includeScope ?? "";
303
402
 
304
403
  if (!parent) {
305
404
  // Root entry: prefix with mount index and use mount-scoped counter
306
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
405
+ const counterKey = mountPrefix
406
+ ? `${mountPrefix}_root_${type}`
407
+ : `root_${type}`;
307
408
  store.counters[counterKey] ??= 0;
308
409
  const index = store.counters[counterKey];
309
410
  store.counters[counterKey] = index + 1;
310
411
  return `${mountPrefix}${prefix}${index}`;
311
412
  } else {
312
- // Child entry: use parent-scoped counter (parent already has M prefix)
313
- const counterKey = `${parent.shortCode}_${type}`;
413
+ // Child entry: use parent-scoped counter with includeScope appended.
414
+ // When we're evaluating a lazy include's direct children, includeScope
415
+ // is a per-include token like "I0" / "I1I0" that partitions the
416
+ // parent's counter namespace so routes inside one include cannot
417
+ // collide with siblings declared outside it.
418
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
314
419
  store.counters[counterKey] ??= 0;
315
420
  const index = store.counters[counterKey];
316
421
  store.counters[counterKey] = index + 1;
317
- return `${parent.shortCode}${prefix}${index}`;
422
+ return `${parent.shortCode}${includeScope}${prefix}${index}`;
318
423
  }
319
424
  },
320
425
  runWithStore: <T>(
321
426
  store: HelperContext,
322
427
  namespace: string,
323
428
  parent: EntryData | null,
324
- callback: (...args: any[]) => T
429
+ callback: (...args: any[]) => T,
325
430
  ): T => {
326
431
  return context.run(
327
432
  {
@@ -335,24 +440,33 @@ export const getContext = (): {
335
440
  isSSR: store.isSSR,
336
441
  patterns: store.patterns,
337
442
  trailingSlash: store.trailingSlash,
443
+ searchSchemas: store.searchSchemas,
338
444
  urlPrefix: store.urlPrefix,
339
445
  namePrefix: store.namePrefix,
446
+ rootScoped: store.rootScoped,
340
447
  trackedIncludes: store.trackedIncludes,
448
+ cacheProfiles: store.cacheProfiles,
449
+ includeScope: store.includeScope,
341
450
  },
342
- callback
451
+ callback,
343
452
  );
344
453
  },
345
454
  run: <T>(
346
455
  namespace: string,
347
456
  parent: EntryData | null,
348
- callback: (...args: any[]) => T
457
+ callback: (...args: any[]) => T,
349
458
  ) => {
350
459
  const store = context.getStore();
351
460
  // Preserve parent counters to ensure globally unique shortCodes
352
461
  const counters = store?.counters || {};
353
462
  const manifest = store ? store.manifest : new Map<string, EntryData>();
354
463
  const patterns = store?.patterns || new Map<string, string>();
355
- const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
464
+ const patternsByPrefix = store?.patternsByPrefix;
465
+ const trailingSlash =
466
+ store?.trailingSlash ||
467
+ new Map<string, "never" | "always" | "ignore">();
468
+ const searchSchemas =
469
+ store?.searchSchemas || new Map<string, Record<string, string>>();
356
470
  return context.run(
357
471
  {
358
472
  manifest,
@@ -364,12 +478,16 @@ export const getContext = (): {
364
478
  metrics: store?.metrics,
365
479
  isSSR: store?.isSSR,
366
480
  patterns,
481
+ patternsByPrefix,
367
482
  trailingSlash,
483
+ searchSchemas,
368
484
  urlPrefix: store?.urlPrefix,
369
485
  namePrefix: store?.namePrefix,
486
+ rootScoped: store?.rootScoped,
370
487
  trackedIncludes: store?.trackedIncludes,
488
+ cacheProfiles: store?.cacheProfiles,
371
489
  },
372
- callback
490
+ callback,
373
491
  );
374
492
  },
375
493
  };
@@ -382,30 +500,61 @@ export const getContext = (): {
382
500
  export function runWithPrefixes<T>(
383
501
  urlPrefix: string,
384
502
  namePrefix: string | undefined,
385
- callback: () => T
503
+ callback: () => T,
386
504
  ): T {
387
505
  const store = RSCRouterContext.getStore();
388
506
  if (!store) {
389
507
  throw new Error("runWithPrefixes must be called within router context");
390
508
  }
391
509
 
392
- // Combine prefixes if there are existing ones
393
- const combinedUrlPrefix = store.urlPrefix
394
- ? `${store.urlPrefix}${urlPrefix}`
395
- : urlPrefix;
396
- const combinedNamePrefix = namePrefix
397
- ? store.namePrefix
398
- ? `${store.namePrefix}.${namePrefix}`
399
- : namePrefix
400
- : store.namePrefix;
510
+ // Combine prefixes if there are existing ones, avoiding double slashes
511
+ let combinedUrlPrefix: string;
512
+ if (store.urlPrefix) {
513
+ if (store.urlPrefix.endsWith("/") && urlPrefix.startsWith("/")) {
514
+ combinedUrlPrefix = store.urlPrefix + urlPrefix.slice(1);
515
+ } else {
516
+ combinedUrlPrefix = store.urlPrefix + urlPrefix;
517
+ }
518
+ } else {
519
+ combinedUrlPrefix = urlPrefix;
520
+ }
521
+ const combinedNamePrefix =
522
+ namePrefix !== undefined
523
+ ? namePrefix === ""
524
+ ? store.namePrefix
525
+ : store.namePrefix
526
+ ? `${store.namePrefix}.${namePrefix}`
527
+ : namePrefix
528
+ : store.namePrefix;
529
+
530
+ // Track root scope for dot-local reverse resolution.
531
+ //
532
+ // The flag answers: "can this route reach bare names at root scope?"
533
+ // It propagates through the include chain:
534
+ //
535
+ // { name: "" } — transparent: inherit parent, default true
536
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
537
+ // no name — inherit parent unchanged
538
+ //
539
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
540
+ // (the outer transparent include establishes root access, and the inner
541
+ // named include inherits it). But a direct { name: "sub" } at root gets
542
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
543
+ const combinedRootScoped =
544
+ namePrefix === ""
545
+ ? (store.rootScoped ?? true)
546
+ : namePrefix !== undefined
547
+ ? (store.rootScoped ?? false)
548
+ : store.rootScoped;
401
549
 
402
550
  return RSCRouterContext.run(
403
551
  {
404
552
  ...store,
405
553
  urlPrefix: combinedUrlPrefix,
406
554
  namePrefix: combinedNamePrefix,
555
+ rootScoped: combinedRootScoped,
407
556
  },
408
- callback
557
+ callback,
409
558
  );
410
559
  }
411
560
 
@@ -425,9 +574,92 @@ export function getNamePrefix(): string | undefined {
425
574
  return store?.namePrefix;
426
575
  }
427
576
 
577
+ /**
578
+ * Get whether the current scope is at root level (no named include boundary above).
579
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
580
+ */
581
+ export function getRootScoped(): boolean {
582
+ const store = RSCRouterContext.getStore();
583
+ return store?.rootScoped ?? true;
584
+ }
585
+
428
586
  // Export HelperContext type for use in other modules
429
587
  export type { HelperContext };
430
588
 
589
+ /**
590
+ * Return an isolated copy of a lazy include's captured parent entry.
591
+ *
592
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
593
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
594
+ * parent, so without isolation one include's loaders/middleware leak into
595
+ * every other route that shares that root.
596
+ *
597
+ * The clone is shallow: only the mutable arrays are copied so each
598
+ * include pushes to its own list. The rest of the entry (id, shortCode,
599
+ * parent pointer, handler) stays shared, which is correct and cheap.
600
+ */
601
+ export function getIsolatedLazyParent(
602
+ captured: EntryData | null | undefined,
603
+ ): EntryData | null {
604
+ if (!captured) return null;
605
+ return {
606
+ ...captured,
607
+ loader: [...captured.loader],
608
+ middleware: [...captured.middleware],
609
+ revalidate: [...captured.revalidate],
610
+ errorBoundary: [...captured.errorBoundary],
611
+ notFoundBoundary: [...captured.notFoundBoundary],
612
+ layout: [...captured.layout],
613
+ parallel: { ...captured.parallel },
614
+ intercept: [...captured.intercept],
615
+ };
616
+ }
617
+
618
+ export function getParallelEntries(
619
+ parallels: ParallelEntries | EntryData[] | undefined,
620
+ ): ParallelEntryData[] {
621
+ if (!parallels) return [];
622
+ if (Array.isArray(parallels)) {
623
+ return parallels.filter(
624
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
625
+ );
626
+ }
627
+ return Object.values(parallels).filter(
628
+ (entry): entry is ParallelEntryData => !!entry,
629
+ );
630
+ }
631
+
632
+ export function getParallelSlotEntries(
633
+ parallels: ParallelEntries | EntryData[] | undefined,
634
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
635
+ if (!parallels) return [];
636
+
637
+ if (Array.isArray(parallels)) {
638
+ return getParallelEntries(parallels).flatMap((entry) =>
639
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
640
+ slot,
641
+ entry,
642
+ })),
643
+ );
644
+ }
645
+
646
+ return Object.entries(parallels)
647
+ .filter(([, entry]) => !!entry)
648
+ .map(([slot, entry]) => ({
649
+ slot: slot as `@${string}`,
650
+ entry: entry!,
651
+ }));
652
+ }
653
+
654
+ export function getParallelSlotCount(
655
+ parallels: ParallelEntries | EntryData[] | undefined,
656
+ ): number {
657
+ if (!parallels) return 0;
658
+ return Array.isArray(parallels)
659
+ ? parallels.filter((entry) => entry?.type === "parallel").length
660
+ : Object.keys(parallels).length;
661
+ }
662
+
431
663
  // ============================================================================
432
664
  // Performance Metrics Helpers
433
665
  // ============================================================================
@@ -443,7 +675,7 @@ export type { HelperContext };
443
675
  * done(); // Records duration
444
676
  * ```
445
677
  */
446
- export function track(label: string): () => void {
678
+ export function track(label: string, depth?: number): () => void {
447
679
  const store = RSCRouterContext.getStore();
448
680
 
449
681
  // No-op if context unavailable or metrics not enabled
@@ -454,7 +686,55 @@ export function track(label: string): () => void {
454
686
  const startTime = performance.now() - store.metrics.requestStart;
455
687
 
456
688
  return () => {
457
- const duration = performance.now() - store.metrics!.requestStart - startTime;
458
- store.metrics!.metrics.push({ label, duration, startTime });
689
+ const duration =
690
+ performance.now() - store.metrics!.requestStart - startTime;
691
+ store.metrics!.metrics.push({
692
+ label,
693
+ duration,
694
+ startTime,
695
+ ...(depth != null ? { depth } : {}),
696
+ });
459
697
  };
460
698
  }
699
+
700
+ /**
701
+ * Separate ALS for tracking loader execution scope.
702
+ * Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
703
+ * nested RSCRouterContext.run() calls in Vite's module runner.
704
+ */
705
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
706
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
707
+ globalThis as any
708
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
709
+
710
+ /**
711
+ * Check if the current execution is inside a cache() DSL boundary.
712
+ * Returns false inside loader execution — loaders are always fresh
713
+ * (never cached), so non-cacheable reads are safe.
714
+ */
715
+ export function isInsideCacheScope(): boolean {
716
+ if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
717
+ // Loaders are always fresh — even inside a cache() boundary, the loader
718
+ // function re-executes on every request. Skip the guard when running
719
+ // inside a loader.
720
+ if (loaderScopeALS.getStore()?.active) return false;
721
+ return true;
722
+ }
723
+
724
+ /**
725
+ * Check if the current execution is inside a DSL loader scope
726
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
727
+ * to distinguish DSL loaders from handler-invoked loaders.
728
+ */
729
+ export function isInsideLoaderScope(): boolean {
730
+ return loaderScopeALS.getStore()?.active === true;
731
+ }
732
+
733
+ /**
734
+ * Run `fn` inside a loader scope. While active, cache-scope guards
735
+ * are bypassed because loaders are always fresh (never cached) and
736
+ * their side effects (setCookie, header, etc.) are safe.
737
+ */
738
+ export function runInsideLoaderScope<T>(fn: () => T): T {
739
+ return loaderScopeALS.run({ active: true }, fn);
740
+ }