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

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -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/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /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,17 +143,18 @@ 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
 
144
160
  export type EntryPropSegments = {
@@ -151,32 +167,57 @@ export type EntryPropSegments = {
151
167
  export type EntryData =
152
168
  | ({
153
169
  type: "route";
154
- handler: Handler<any, any>;
170
+ handler: Handler<any, any, any>;
155
171
  loading?: ReactNode | false;
172
+ transition?: TransitionConfig;
156
173
  /** URL pattern for this route (used by path() in urls()) */
157
174
  pattern?: string;
175
+ /** Set when handler is a Prerender definition */
176
+ isPrerender?: true;
177
+ /** Original PrerenderHandlerDefinition (for build-time getParams access) */
178
+ prerenderDef?: {
179
+ getParams?: (ctx: any) => Promise<any[]> | any[];
180
+ options?: { passthrough?: boolean };
181
+ };
182
+ /** Set when handler is a Static definition (build-time only) */
183
+ isStaticPrerender?: true;
184
+ /** Static handler $$id for build-time store lookup */
185
+ staticHandlerId?: string;
186
+ /** Response type for non-RSC routes (json, text, image, any) */
187
+ responseType?: string;
158
188
  } & EntryPropCommon &
159
189
  EntryPropDatas &
160
190
  EntryPropSegments)
161
191
  | ({
162
192
  type: "layout";
163
- handler: ReactNode | Handler<any, any>;
193
+ handler: ReactNode | Handler<any, any, any>;
164
194
  loading?: ReactNode | false;
195
+ transition?: TransitionConfig;
196
+ /** Set when handler is a Static definition (build-time only) */
197
+ isStaticPrerender?: true;
198
+ /** Static handler $$id for build-time store lookup */
199
+ staticHandlerId?: string;
165
200
  } & EntryPropCommon &
166
201
  EntryPropDatas &
167
202
  EntryPropSegments)
168
203
  | ({
169
204
  type: "parallel";
170
- handler: Record<`@${string}`, Handler<any, any> | ReactNode>;
205
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
171
206
  loading?: ReactNode | false;
207
+ transition?: TransitionConfig;
208
+ /** Set when any parallel slot is a Static definition */
209
+ isStaticPrerender?: true;
210
+ /** Per-slot static handler $$ids for build-time store lookup */
211
+ staticHandlerIds?: Record<string, string>;
172
212
  } & EntryPropCommon &
173
213
  EntryPropDatas &
174
214
  EntryPropSegments)
175
215
  | ({
176
216
  type: "cache";
177
217
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
178
- handler: ReactNode | Handler<any, any>;
218
+ handler: ReactNode | Handler<any, any, any>;
179
219
  loading?: ReactNode | false;
220
+ transition?: TransitionConfig;
180
221
  } & EntryPropCommon &
181
222
  EntryPropDatas &
182
223
  EntryPropSegments);
@@ -211,17 +252,34 @@ interface HelperContext {
211
252
  patternsByPrefix?: Map<string, Map<string, string>>;
212
253
  /** Trailing slash config per route name */
213
254
  trailingSlash?: Map<string, "never" | "always" | "ignore">;
255
+ /** Search param schemas per route name */
256
+ searchSchemas?: Map<string, Record<string, string>>;
214
257
  /** URL prefix from include() - applied to all path() patterns */
215
258
  urlPrefix?: string;
216
259
  /** Name prefix from include() - applied to all named routes */
217
260
  namePrefix?: string;
261
+ /** True when this scope is at root level (no named include boundary above).
262
+ * Routes at root scope allow dot-local reverse to fall back to bare names. */
263
+ rootScoped?: boolean;
218
264
  /** Run helper for cleaner middleware code */
219
265
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
220
266
  /** Tracked includes for build-time manifest generation */
221
267
  trackedIncludes?: TrackedInclude[];
268
+ /** Cache profiles for DSL-time cache("profileName") resolution */
269
+ cacheProfiles?: Record<
270
+ string,
271
+ import("../cache/profile-registry.js").CacheProfile
272
+ >;
222
273
  }
223
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
224
- new AsyncLocalStorage<HelperContext>();
274
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
275
+ // module re-evaluation. Without this, Vite's RSC module runner may create
276
+ // a new instance when context.ts is re-evaluated, while other modules still
277
+ // hold references to the old instance — causing getStore() to return
278
+ // undefined even inside a run() callback.
279
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
280
+ export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
281
+ globalThis as any
282
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
225
283
 
226
284
  export const getContext = (): {
227
285
  context: AsyncLocalStorage<HelperContext>;
@@ -229,21 +287,21 @@ export const getContext = (): {
229
287
  getParent: () => EntryData | null;
230
288
  getOrCreateStore: (forRoute?: string) => HelperContext;
231
289
  getNextIndex: (
232
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
290
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
233
291
  ) => string;
234
292
  getShortCode: (
235
- type: "layout" | "parallel" | "route" | "loader" | "cache"
293
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
236
294
  ) => string;
237
295
  run: <T>(
238
296
  namespace: string,
239
297
  parent: EntryData | null,
240
- callback: (...args: any[]) => T
298
+ callback: (...args: any[]) => T,
241
299
  ) => T;
242
300
  runWithStore: <T>(
243
301
  store: HelperContext,
244
302
  namespace: string,
245
303
  parent: EntryData | null,
246
- callback: (...args: any[]) => T
304
+ callback: (...args: any[]) => T,
247
305
  ) => T;
248
306
  } => {
249
307
  const context = RSCRouterContext;
@@ -262,6 +320,7 @@ export const getContext = (): {
262
320
  patterns: new Map<string, string>(),
263
321
  patternsByPrefix: new Map<string, Map<string, string>>(),
264
322
  trailingSlash: new Map<string, "never" | "always" | "ignore">(),
323
+ searchSchemas: new Map<string, Record<string, string>>(),
265
324
  } satisfies HelperContext;
266
325
  }
267
326
  return store;
@@ -270,7 +329,7 @@ export const getContext = (): {
270
329
  const store = context.getStore();
271
330
  if (!store) {
272
331
  throw new Error(
273
- "RSC Router context store is not available. Make sure to run within RSC Router context."
332
+ "RSC Router context store is not available. Make sure to run within RSC Router context.",
274
333
  );
275
334
  }
276
335
  return store;
@@ -284,7 +343,7 @@ export const getContext = (): {
284
343
  return store.parent;
285
344
  },
286
345
  getNextIndex: (
287
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
346
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
288
347
  ) => {
289
348
  const store = context.getStore();
290
349
  invariant(store, "No context RSCRouterContext available");
@@ -293,17 +352,31 @@ export const getContext = (): {
293
352
  store.counters[type] = index + 1;
294
353
  return `$${type}.${index}`;
295
354
  },
296
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
355
+ getShortCode: (
356
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
357
+ ) => {
297
358
  const store = context.getStore();
298
359
  invariant(store, "No context RSCRouterContext available");
299
360
 
300
361
  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}` : "";
362
+ const prefix =
363
+ type === "layout"
364
+ ? "L"
365
+ : type === "parallel"
366
+ ? "P"
367
+ : type === "loader"
368
+ ? "D"
369
+ : type === "cache"
370
+ ? "C"
371
+ : "R";
372
+ const mountPrefix =
373
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
303
374
 
304
375
  if (!parent) {
305
376
  // Root entry: prefix with mount index and use mount-scoped counter
306
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
377
+ const counterKey = mountPrefix
378
+ ? `${mountPrefix}_root_${type}`
379
+ : `root_${type}`;
307
380
  store.counters[counterKey] ??= 0;
308
381
  const index = store.counters[counterKey];
309
382
  store.counters[counterKey] = index + 1;
@@ -321,7 +394,7 @@ export const getContext = (): {
321
394
  store: HelperContext,
322
395
  namespace: string,
323
396
  parent: EntryData | null,
324
- callback: (...args: any[]) => T
397
+ callback: (...args: any[]) => T,
325
398
  ): T => {
326
399
  return context.run(
327
400
  {
@@ -335,24 +408,32 @@ export const getContext = (): {
335
408
  isSSR: store.isSSR,
336
409
  patterns: store.patterns,
337
410
  trailingSlash: store.trailingSlash,
411
+ searchSchemas: store.searchSchemas,
338
412
  urlPrefix: store.urlPrefix,
339
413
  namePrefix: store.namePrefix,
414
+ rootScoped: store.rootScoped,
340
415
  trackedIncludes: store.trackedIncludes,
416
+ cacheProfiles: store.cacheProfiles,
341
417
  },
342
- callback
418
+ callback,
343
419
  );
344
420
  },
345
421
  run: <T>(
346
422
  namespace: string,
347
423
  parent: EntryData | null,
348
- callback: (...args: any[]) => T
424
+ callback: (...args: any[]) => T,
349
425
  ) => {
350
426
  const store = context.getStore();
351
427
  // Preserve parent counters to ensure globally unique shortCodes
352
428
  const counters = store?.counters || {};
353
429
  const manifest = store ? store.manifest : new Map<string, EntryData>();
354
430
  const patterns = store?.patterns || new Map<string, string>();
355
- const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
431
+ const patternsByPrefix = store?.patternsByPrefix;
432
+ const trailingSlash =
433
+ store?.trailingSlash ||
434
+ new Map<string, "never" | "always" | "ignore">();
435
+ const searchSchemas =
436
+ store?.searchSchemas || new Map<string, Record<string, string>>();
356
437
  return context.run(
357
438
  {
358
439
  manifest,
@@ -364,12 +445,16 @@ export const getContext = (): {
364
445
  metrics: store?.metrics,
365
446
  isSSR: store?.isSSR,
366
447
  patterns,
448
+ patternsByPrefix,
367
449
  trailingSlash,
450
+ searchSchemas,
368
451
  urlPrefix: store?.urlPrefix,
369
452
  namePrefix: store?.namePrefix,
453
+ rootScoped: store?.rootScoped,
370
454
  trackedIncludes: store?.trackedIncludes,
455
+ cacheProfiles: store?.cacheProfiles,
371
456
  },
372
- callback
457
+ callback,
373
458
  );
374
459
  },
375
460
  };
@@ -382,30 +467,61 @@ export const getContext = (): {
382
467
  export function runWithPrefixes<T>(
383
468
  urlPrefix: string,
384
469
  namePrefix: string | undefined,
385
- callback: () => T
470
+ callback: () => T,
386
471
  ): T {
387
472
  const store = RSCRouterContext.getStore();
388
473
  if (!store) {
389
474
  throw new Error("runWithPrefixes must be called within router context");
390
475
  }
391
476
 
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;
477
+ // Combine prefixes if there are existing ones, avoiding double slashes
478
+ let combinedUrlPrefix: string;
479
+ if (store.urlPrefix) {
480
+ if (store.urlPrefix.endsWith("/") && urlPrefix.startsWith("/")) {
481
+ combinedUrlPrefix = store.urlPrefix + urlPrefix.slice(1);
482
+ } else {
483
+ combinedUrlPrefix = store.urlPrefix + urlPrefix;
484
+ }
485
+ } else {
486
+ combinedUrlPrefix = urlPrefix;
487
+ }
488
+ const combinedNamePrefix =
489
+ namePrefix !== undefined
490
+ ? namePrefix === ""
491
+ ? store.namePrefix
492
+ : store.namePrefix
493
+ ? `${store.namePrefix}.${namePrefix}`
494
+ : namePrefix
495
+ : store.namePrefix;
496
+
497
+ // Track root scope for dot-local reverse resolution.
498
+ //
499
+ // The flag answers: "can this route reach bare names at root scope?"
500
+ // It propagates through the include chain:
501
+ //
502
+ // { name: "" } — transparent: inherit parent, default true
503
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
504
+ // no name — inherit parent unchanged
505
+ //
506
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
507
+ // (the outer transparent include establishes root access, and the inner
508
+ // named include inherits it). But a direct { name: "sub" } at root gets
509
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
510
+ const combinedRootScoped =
511
+ namePrefix === ""
512
+ ? (store.rootScoped ?? true)
513
+ : namePrefix !== undefined
514
+ ? (store.rootScoped ?? false)
515
+ : store.rootScoped;
401
516
 
402
517
  return RSCRouterContext.run(
403
518
  {
404
519
  ...store,
405
520
  urlPrefix: combinedUrlPrefix,
406
521
  namePrefix: combinedNamePrefix,
522
+ rootScoped: combinedRootScoped,
407
523
  },
408
- callback
524
+ callback,
409
525
  );
410
526
  }
411
527
 
@@ -425,6 +541,15 @@ export function getNamePrefix(): string | undefined {
425
541
  return store?.namePrefix;
426
542
  }
427
543
 
544
+ /**
545
+ * Get whether the current scope is at root level (no named include boundary above).
546
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
547
+ */
548
+ export function getRootScoped(): boolean {
549
+ const store = RSCRouterContext.getStore();
550
+ return store?.rootScoped ?? true;
551
+ }
552
+
428
553
  // Export HelperContext type for use in other modules
429
554
  export type { HelperContext };
430
555
 
@@ -443,7 +568,7 @@ export type { HelperContext };
443
568
  * done(); // Records duration
444
569
  * ```
445
570
  */
446
- export function track(label: string): () => void {
571
+ export function track(label: string, depth?: number): () => void {
447
572
  const store = RSCRouterContext.getStore();
448
573
 
449
574
  // No-op if context unavailable or metrics not enabled
@@ -454,7 +579,13 @@ export function track(label: string): () => void {
454
579
  const startTime = performance.now() - store.metrics.requestStart;
455
580
 
456
581
  return () => {
457
- const duration = performance.now() - store.metrics!.requestStart - startTime;
458
- store.metrics!.metrics.push({ label, duration, startTime });
582
+ const duration =
583
+ performance.now() - store.metrics!.requestStart - startTime;
584
+ store.metrics!.metrics.push({
585
+ label,
586
+ duration,
587
+ startTime,
588
+ ...(depth != null ? { depth } : {}),
589
+ });
459
590
  };
460
591
  }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Cookie Store — Next.js-style cookie facade backed by the response-derived model.
3
+ *
4
+ * `cookies()` returns a CookieStore scoped to the current request.
5
+ * Reads merge the original Cookie header with Set-Cookie mutations
6
+ * already queued on the response stub (last-write-wins).
7
+ * Writes append Set-Cookie to the response stub.
8
+ */
9
+
10
+ import type { CookieOptions } from "../router/middleware-types.js";
11
+ import { getRequestContext } from "./request-context.js";
12
+ import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
+
14
+ /**
15
+ * A single cookie entry returned by get() and getAll().
16
+ */
17
+ export interface Cookie {
18
+ name: string;
19
+ value: string;
20
+ }
21
+
22
+ /**
23
+ * Request-scoped cookie store.
24
+ *
25
+ * Reads see the effective merged view (original request + same-request mutations).
26
+ * Writes append Set-Cookie headers to the shared response stub.
27
+ */
28
+ export interface CookieStore {
29
+ /** Get a single cookie by name. Returns undefined if not set or deleted. */
30
+ get(name: string): Cookie | undefined;
31
+
32
+ /** Get all effective cookies, or all cookies with a given name. */
33
+ getAll(name?: string): Cookie[];
34
+
35
+ /** Check whether a cookie exists in the effective view. */
36
+ has(name: string): boolean;
37
+
38
+ /** Set a cookie (appends Set-Cookie to the response stub). */
39
+ set(name: string, value: string, options?: CookieOptions): void;
40
+
41
+ /** Delete a cookie (appends Set-Cookie with maxAge=0 to the response stub). */
42
+ delete(name: string, options?: Pick<CookieOptions, "domain" | "path">): void;
43
+ }
44
+
45
+ /**
46
+ * Get the request-scoped cookie store.
47
+ *
48
+ * Must be called inside a request context (middleware, handler, loader, action).
49
+ * Throws if called outside request scope.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * import { cookies } from "@rangojs/router";
54
+ *
55
+ * // In a handler, loader, or action:
56
+ * const session = cookies().get("session")?.value;
57
+ * cookies().set("session", "new-token", { httpOnly: true });
58
+ * cookies().delete("session");
59
+ * ```
60
+ */
61
+ export function cookies(): CookieStore {
62
+ const ctx = getRequestContext();
63
+ assertNotInsideCacheContext(ctx, "cookies");
64
+ return createCookieStore(ctx);
65
+ }
66
+
67
+ /**
68
+ * Read-only view of HTTP headers.
69
+ * Exposes only the read methods of the Headers API.
70
+ */
71
+ export interface ReadonlyHeaders {
72
+ get(name: string): string | null;
73
+ has(name: string): boolean;
74
+ entries(): HeadersIterator<[string, string]>;
75
+ keys(): HeadersIterator<string>;
76
+ values(): HeadersIterator<string>;
77
+ forEach(
78
+ callback: (value: string, name: string, parent: ReadonlyHeaders) => void,
79
+ ): void;
80
+ [Symbol.iterator](): HeadersIterator<[string, string]>;
81
+ }
82
+
83
+ // Minimal iterator interface (avoids pulling IterableIterator from lib.dom)
84
+ type HeadersIterator<T> = IterableIterator<T>;
85
+
86
+ /**
87
+ * Throw if called inside a "use cache" function.
88
+ * Reading request-scoped data (cookies, headers) inside a cached function
89
+ * produces results that vary per request but the cache key does not include
90
+ * those values, leading to one user's data being served to another.
91
+ */
92
+ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
+ if (
94
+ ctx !== null &&
95
+ ctx !== undefined &&
96
+ typeof ctx === "object" &&
97
+ (INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
98
+ ) {
99
+ throw new Error(
100
+ `${fnName}() cannot be called inside a "use cache" function. ` +
101
+ `Request-scoped data (cookies, headers) varies per request but is not ` +
102
+ `reflected in the cache key, so cached results would be served to the ` +
103
+ `wrong users. Extract the value before the cached function and pass it ` +
104
+ `as an argument:\n\n` +
105
+ ` const locale = cookies().get("locale")?.value ?? "en";\n` +
106
+ ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
+ );
108
+ }
109
+ }
110
+
111
+ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
112
+
113
+ /**
114
+ * Get the original request headers (read-only).
115
+ *
116
+ * Must be called inside a request context.
117
+ * Returns a read-only view of the incoming request's headers.
118
+ * Mutation methods (set, append, delete) throw at runtime.
119
+ *
120
+ * @example
121
+ * ```typescript
122
+ * import { headers } from "@rangojs/router";
123
+ *
124
+ * const auth = headers().get("authorization");
125
+ * const contentType = headers().get("content-type");
126
+ * ```
127
+ */
128
+ export function headers(): ReadonlyHeaders {
129
+ const ctx = getRequestContext();
130
+ assertNotInsideCacheContext(ctx, "headers");
131
+ return new Proxy(ctx.request.headers, {
132
+ get(target, prop, receiver) {
133
+ if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
134
+ return () => {
135
+ throw new Error(
136
+ `headers().${prop}() is not allowed. headers() returns a read-only view of request headers. ` +
137
+ `Use ctx.header() to set response headers.`,
138
+ );
139
+ };
140
+ }
141
+ const value = Reflect.get(target, prop, receiver);
142
+ return typeof value === "function" ? value.bind(target) : value;
143
+ },
144
+ }) as unknown as ReadonlyHeaders;
145
+ }
146
+
147
+ /**
148
+ * Create a CookieStore backed by a RequestContext.
149
+ * @internal Shared between cookies() shorthand and context methods.
150
+ */
151
+ function createCookieStore(ctx: {
152
+ cookie(name: string): string | undefined;
153
+ cookies(): Record<string, string>;
154
+ setCookie(name: string, value: string, options?: CookieOptions): void;
155
+ deleteCookie(
156
+ name: string,
157
+ options?: Pick<CookieOptions, "domain" | "path">,
158
+ ): void;
159
+ }): CookieStore {
160
+ return {
161
+ get(name: string): Cookie | undefined {
162
+ const value = ctx.cookie(name);
163
+ return value !== undefined ? { name, value } : undefined;
164
+ },
165
+
166
+ getAll(name?: string): Cookie[] {
167
+ const all = ctx.cookies();
168
+ if (name !== undefined) {
169
+ const value = all[name];
170
+ return value !== undefined ? [{ name, value }] : [];
171
+ }
172
+ return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
173
+ },
174
+
175
+ has(name: string): boolean {
176
+ return ctx.cookie(name) !== undefined;
177
+ },
178
+
179
+ set(name: string, value: string, options?: CookieOptions): void {
180
+ ctx.setCookie(name, value, options);
181
+ },
182
+
183
+ delete(
184
+ name: string,
185
+ options?: Pick<CookieOptions, "domain" | "path">,
186
+ ): void {
187
+ ctx.deleteCookie(name, options);
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Fetchable loader store - internal registry for fetchable loader functions.
3
+ *
4
+ * Extracted into its own module to avoid circular dependencies between
5
+ * loader.rsc.ts and request-context.ts. This module has no imports from
6
+ * either, so both can safely import from here.
7
+ *
8
+ * Populated by createLoader() in loader.rsc.ts.
9
+ * Read by request-context.ts (for ctx.use()) and loader-registry.ts (for GET-based fetching).
10
+ */
11
+
12
+ import type { LoaderFn } from "../types.js";
13
+ import type { MiddlewareFn } from "../router/middleware.js";
14
+
15
+ export interface LoaderRegistryEntry {
16
+ fn: LoaderFn<any, any, any>;
17
+ middleware: MiddlewareFn[];
18
+ /** Whether this loader is fetchable via the _rsc_loader endpoint. */
19
+ fetchable: boolean;
20
+ }
21
+
22
+ const fetchableLoaderRegistry = new Map<string, LoaderRegistryEntry>();
23
+
24
+ export function registerFetchableLoader(
25
+ id: string,
26
+ fn: LoaderFn<any, any, any>,
27
+ middleware: MiddlewareFn[],
28
+ fetchable: boolean,
29
+ ): void {
30
+ fetchableLoaderRegistry.set(id, { fn, middleware, fetchable });
31
+ }
32
+
33
+ export function getFetchableLoader(
34
+ id: string,
35
+ ): LoaderRegistryEntry | undefined {
36
+ return fetchableLoaderRegistry.get(id);
37
+ }