@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.71

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 (307) 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 +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +135 -35
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +748 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1379 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +151 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /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,37 @@ 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;
222
283
  }
223
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
224
- new AsyncLocalStorage<HelperContext>();
284
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
285
+ // module re-evaluation. Without this, Vite's RSC module runner may create
286
+ // a new instance when context.ts is re-evaluated, while other modules still
287
+ // hold references to the old instance — causing getStore() to return
288
+ // undefined even inside a run() callback.
289
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
290
+ export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
291
+ globalThis as any
292
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
225
293
 
226
294
  export const getContext = (): {
227
295
  context: AsyncLocalStorage<HelperContext>;
@@ -229,21 +297,21 @@ export const getContext = (): {
229
297
  getParent: () => EntryData | null;
230
298
  getOrCreateStore: (forRoute?: string) => HelperContext;
231
299
  getNextIndex: (
232
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
300
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
233
301
  ) => string;
234
302
  getShortCode: (
235
- type: "layout" | "parallel" | "route" | "loader" | "cache"
303
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
236
304
  ) => string;
237
305
  run: <T>(
238
306
  namespace: string,
239
307
  parent: EntryData | null,
240
- callback: (...args: any[]) => T
308
+ callback: (...args: any[]) => T,
241
309
  ) => T;
242
310
  runWithStore: <T>(
243
311
  store: HelperContext,
244
312
  namespace: string,
245
313
  parent: EntryData | null,
246
- callback: (...args: any[]) => T
314
+ callback: (...args: any[]) => T,
247
315
  ) => T;
248
316
  } => {
249
317
  const context = RSCRouterContext;
@@ -262,6 +330,7 @@ export const getContext = (): {
262
330
  patterns: new Map<string, string>(),
263
331
  patternsByPrefix: new Map<string, Map<string, string>>(),
264
332
  trailingSlash: new Map<string, "never" | "always" | "ignore">(),
333
+ searchSchemas: new Map<string, Record<string, string>>(),
265
334
  } satisfies HelperContext;
266
335
  }
267
336
  return store;
@@ -270,7 +339,7 @@ export const getContext = (): {
270
339
  const store = context.getStore();
271
340
  if (!store) {
272
341
  throw new Error(
273
- "RSC Router context store is not available. Make sure to run within RSC Router context."
342
+ "RSC Router context store is not available. Make sure to run within RSC Router context.",
274
343
  );
275
344
  }
276
345
  return store;
@@ -284,7 +353,7 @@ export const getContext = (): {
284
353
  return store.parent;
285
354
  },
286
355
  getNextIndex: (
287
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
356
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
288
357
  ) => {
289
358
  const store = context.getStore();
290
359
  invariant(store, "No context RSCRouterContext available");
@@ -293,17 +362,31 @@ export const getContext = (): {
293
362
  store.counters[type] = index + 1;
294
363
  return `$${type}.${index}`;
295
364
  },
296
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
365
+ getShortCode: (
366
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
367
+ ) => {
297
368
  const store = context.getStore();
298
369
  invariant(store, "No context RSCRouterContext available");
299
370
 
300
371
  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}` : "";
372
+ const prefix =
373
+ type === "layout"
374
+ ? "L"
375
+ : type === "parallel"
376
+ ? "P"
377
+ : type === "loader"
378
+ ? "D"
379
+ : type === "cache"
380
+ ? "C"
381
+ : "R";
382
+ const mountPrefix =
383
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
303
384
 
304
385
  if (!parent) {
305
386
  // Root entry: prefix with mount index and use mount-scoped counter
306
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
387
+ const counterKey = mountPrefix
388
+ ? `${mountPrefix}_root_${type}`
389
+ : `root_${type}`;
307
390
  store.counters[counterKey] ??= 0;
308
391
  const index = store.counters[counterKey];
309
392
  store.counters[counterKey] = index + 1;
@@ -321,7 +404,7 @@ export const getContext = (): {
321
404
  store: HelperContext,
322
405
  namespace: string,
323
406
  parent: EntryData | null,
324
- callback: (...args: any[]) => T
407
+ callback: (...args: any[]) => T,
325
408
  ): T => {
326
409
  return context.run(
327
410
  {
@@ -335,24 +418,32 @@ export const getContext = (): {
335
418
  isSSR: store.isSSR,
336
419
  patterns: store.patterns,
337
420
  trailingSlash: store.trailingSlash,
421
+ searchSchemas: store.searchSchemas,
338
422
  urlPrefix: store.urlPrefix,
339
423
  namePrefix: store.namePrefix,
424
+ rootScoped: store.rootScoped,
340
425
  trackedIncludes: store.trackedIncludes,
426
+ cacheProfiles: store.cacheProfiles,
341
427
  },
342
- callback
428
+ callback,
343
429
  );
344
430
  },
345
431
  run: <T>(
346
432
  namespace: string,
347
433
  parent: EntryData | null,
348
- callback: (...args: any[]) => T
434
+ callback: (...args: any[]) => T,
349
435
  ) => {
350
436
  const store = context.getStore();
351
437
  // Preserve parent counters to ensure globally unique shortCodes
352
438
  const counters = store?.counters || {};
353
439
  const manifest = store ? store.manifest : new Map<string, EntryData>();
354
440
  const patterns = store?.patterns || new Map<string, string>();
355
- const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
441
+ const patternsByPrefix = store?.patternsByPrefix;
442
+ const trailingSlash =
443
+ store?.trailingSlash ||
444
+ new Map<string, "never" | "always" | "ignore">();
445
+ const searchSchemas =
446
+ store?.searchSchemas || new Map<string, Record<string, string>>();
356
447
  return context.run(
357
448
  {
358
449
  manifest,
@@ -364,12 +455,16 @@ export const getContext = (): {
364
455
  metrics: store?.metrics,
365
456
  isSSR: store?.isSSR,
366
457
  patterns,
458
+ patternsByPrefix,
367
459
  trailingSlash,
460
+ searchSchemas,
368
461
  urlPrefix: store?.urlPrefix,
369
462
  namePrefix: store?.namePrefix,
463
+ rootScoped: store?.rootScoped,
370
464
  trackedIncludes: store?.trackedIncludes,
465
+ cacheProfiles: store?.cacheProfiles,
371
466
  },
372
- callback
467
+ callback,
373
468
  );
374
469
  },
375
470
  };
@@ -382,30 +477,61 @@ export const getContext = (): {
382
477
  export function runWithPrefixes<T>(
383
478
  urlPrefix: string,
384
479
  namePrefix: string | undefined,
385
- callback: () => T
480
+ callback: () => T,
386
481
  ): T {
387
482
  const store = RSCRouterContext.getStore();
388
483
  if (!store) {
389
484
  throw new Error("runWithPrefixes must be called within router context");
390
485
  }
391
486
 
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;
487
+ // Combine prefixes if there are existing ones, avoiding double slashes
488
+ let combinedUrlPrefix: string;
489
+ if (store.urlPrefix) {
490
+ if (store.urlPrefix.endsWith("/") && urlPrefix.startsWith("/")) {
491
+ combinedUrlPrefix = store.urlPrefix + urlPrefix.slice(1);
492
+ } else {
493
+ combinedUrlPrefix = store.urlPrefix + urlPrefix;
494
+ }
495
+ } else {
496
+ combinedUrlPrefix = urlPrefix;
497
+ }
498
+ const combinedNamePrefix =
499
+ namePrefix !== undefined
500
+ ? namePrefix === ""
501
+ ? store.namePrefix
502
+ : store.namePrefix
503
+ ? `${store.namePrefix}.${namePrefix}`
504
+ : namePrefix
505
+ : store.namePrefix;
506
+
507
+ // Track root scope for dot-local reverse resolution.
508
+ //
509
+ // The flag answers: "can this route reach bare names at root scope?"
510
+ // It propagates through the include chain:
511
+ //
512
+ // { name: "" } — transparent: inherit parent, default true
513
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
514
+ // no name — inherit parent unchanged
515
+ //
516
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
517
+ // (the outer transparent include establishes root access, and the inner
518
+ // named include inherits it). But a direct { name: "sub" } at root gets
519
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
520
+ const combinedRootScoped =
521
+ namePrefix === ""
522
+ ? (store.rootScoped ?? true)
523
+ : namePrefix !== undefined
524
+ ? (store.rootScoped ?? false)
525
+ : store.rootScoped;
401
526
 
402
527
  return RSCRouterContext.run(
403
528
  {
404
529
  ...store,
405
530
  urlPrefix: combinedUrlPrefix,
406
531
  namePrefix: combinedNamePrefix,
532
+ rootScoped: combinedRootScoped,
407
533
  },
408
- callback
534
+ callback,
409
535
  );
410
536
  }
411
537
 
@@ -425,9 +551,92 @@ export function getNamePrefix(): string | undefined {
425
551
  return store?.namePrefix;
426
552
  }
427
553
 
554
+ /**
555
+ * Get whether the current scope is at root level (no named include boundary above).
556
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
557
+ */
558
+ export function getRootScoped(): boolean {
559
+ const store = RSCRouterContext.getStore();
560
+ return store?.rootScoped ?? true;
561
+ }
562
+
428
563
  // Export HelperContext type for use in other modules
429
564
  export type { HelperContext };
430
565
 
566
+ /**
567
+ * Return an isolated copy of a lazy include's captured parent entry.
568
+ *
569
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
570
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
571
+ * parent, so without isolation one include's loaders/middleware leak into
572
+ * every other route that shares that root.
573
+ *
574
+ * The clone is shallow: only the mutable arrays are copied so each
575
+ * include pushes to its own list. The rest of the entry (id, shortCode,
576
+ * parent pointer, handler) stays shared, which is correct and cheap.
577
+ */
578
+ export function getIsolatedLazyParent(
579
+ captured: EntryData | null | undefined,
580
+ ): EntryData | null {
581
+ if (!captured) return null;
582
+ return {
583
+ ...captured,
584
+ loader: [...captured.loader],
585
+ middleware: [...captured.middleware],
586
+ revalidate: [...captured.revalidate],
587
+ errorBoundary: [...captured.errorBoundary],
588
+ notFoundBoundary: [...captured.notFoundBoundary],
589
+ layout: [...captured.layout],
590
+ parallel: { ...captured.parallel },
591
+ intercept: [...captured.intercept],
592
+ };
593
+ }
594
+
595
+ export function getParallelEntries(
596
+ parallels: ParallelEntries | EntryData[] | undefined,
597
+ ): ParallelEntryData[] {
598
+ if (!parallels) return [];
599
+ if (Array.isArray(parallels)) {
600
+ return parallels.filter(
601
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
602
+ );
603
+ }
604
+ return Object.values(parallels).filter(
605
+ (entry): entry is ParallelEntryData => !!entry,
606
+ );
607
+ }
608
+
609
+ export function getParallelSlotEntries(
610
+ parallels: ParallelEntries | EntryData[] | undefined,
611
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
612
+ if (!parallels) return [];
613
+
614
+ if (Array.isArray(parallels)) {
615
+ return getParallelEntries(parallels).flatMap((entry) =>
616
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
617
+ slot,
618
+ entry,
619
+ })),
620
+ );
621
+ }
622
+
623
+ return Object.entries(parallels)
624
+ .filter(([, entry]) => !!entry)
625
+ .map(([slot, entry]) => ({
626
+ slot: slot as `@${string}`,
627
+ entry: entry!,
628
+ }));
629
+ }
630
+
631
+ export function getParallelSlotCount(
632
+ parallels: ParallelEntries | EntryData[] | undefined,
633
+ ): number {
634
+ if (!parallels) return 0;
635
+ return Array.isArray(parallels)
636
+ ? parallels.filter((entry) => entry?.type === "parallel").length
637
+ : Object.keys(parallels).length;
638
+ }
639
+
431
640
  // ============================================================================
432
641
  // Performance Metrics Helpers
433
642
  // ============================================================================
@@ -443,7 +652,7 @@ export type { HelperContext };
443
652
  * done(); // Records duration
444
653
  * ```
445
654
  */
446
- export function track(label: string): () => void {
655
+ export function track(label: string, depth?: number): () => void {
447
656
  const store = RSCRouterContext.getStore();
448
657
 
449
658
  // No-op if context unavailable or metrics not enabled
@@ -454,7 +663,55 @@ export function track(label: string): () => void {
454
663
  const startTime = performance.now() - store.metrics.requestStart;
455
664
 
456
665
  return () => {
457
- const duration = performance.now() - store.metrics!.requestStart - startTime;
458
- store.metrics!.metrics.push({ label, duration, startTime });
666
+ const duration =
667
+ performance.now() - store.metrics!.requestStart - startTime;
668
+ store.metrics!.metrics.push({
669
+ label,
670
+ duration,
671
+ startTime,
672
+ ...(depth != null ? { depth } : {}),
673
+ });
459
674
  };
460
675
  }
676
+
677
+ /**
678
+ * Separate ALS for tracking loader execution scope.
679
+ * Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
680
+ * nested RSCRouterContext.run() calls in Vite's module runner.
681
+ */
682
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
683
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
684
+ globalThis as any
685
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
686
+
687
+ /**
688
+ * Check if the current execution is inside a cache() DSL boundary.
689
+ * Returns false inside loader execution — loaders are always fresh
690
+ * (never cached), so non-cacheable reads are safe.
691
+ */
692
+ export function isInsideCacheScope(): boolean {
693
+ if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
694
+ // Loaders are always fresh — even inside a cache() boundary, the loader
695
+ // function re-executes on every request. Skip the guard when running
696
+ // inside a loader.
697
+ if (loaderScopeALS.getStore()?.active) return false;
698
+ return true;
699
+ }
700
+
701
+ /**
702
+ * Check if the current execution is inside a DSL loader scope
703
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
704
+ * to distinguish DSL loaders from handler-invoked loaders.
705
+ */
706
+ export function isInsideLoaderScope(): boolean {
707
+ return loaderScopeALS.getStore()?.active === true;
708
+ }
709
+
710
+ /**
711
+ * Run `fn` inside a loader scope. While active, cache-scope guards
712
+ * are bypassed because loaders are always fresh (never cached) and
713
+ * their side effects (setCookie, header, etc.) are safe.
714
+ */
715
+ export function runInsideLoaderScope<T>(fn: () => T): T {
716
+ return loaderScopeALS.run({ active: true }, fn);
717
+ }