@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30

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 (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  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 +111 -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 +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -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 +10 -15
  114. package/src/client.tsx +114 -135
  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 +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /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
  /**
@@ -55,6 +66,8 @@ export type EntryPropCommon = {
55
66
  parent: EntryData | null;
56
67
  /** Cache configuration for this entry (set by cache() DSL) */
57
68
  cache?: EntryCacheConfig;
69
+ /** URL prefix from include() scope, used for MountContext on client */
70
+ mountPath?: string;
58
71
  };
59
72
 
60
73
  /**
@@ -103,12 +116,14 @@ export type InterceptSegmentsState = {
103
116
  * @internal This type is an implementation detail and may change without notice.
104
117
  */
105
118
  export type InterceptSelectorContext<TEnv = any> = {
106
- from: URL; // Source URL (where user is coming from)
107
- to: URL; // Destination URL (where user is navigating to)
108
- params: Record<string, string>; // Matched route params
109
- request: Request; // The HTTP request object
110
- env: TEnv; // Platform bindings (Cloudflare env, etc.)
111
- 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)
112
127
  };
113
128
 
114
129
  /**
@@ -117,7 +132,9 @@ export type InterceptSelectorContext<TEnv = any> = {
117
132
  *
118
133
  * @internal This type is an implementation detail and may change without notice.
119
134
  */
120
- export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>) => boolean;
135
+ export type InterceptWhenFn<TEnv = any> = (
136
+ ctx: InterceptSelectorContext<TEnv>,
137
+ ) => boolean;
121
138
 
122
139
  /**
123
140
  * Intercept entry stored in EntryData
@@ -126,17 +143,18 @@ export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>)
126
143
  * @internal This type is an implementation detail and may change without notice.
127
144
  */
128
145
  export type InterceptEntry = {
129
- slotName: `@${string}`; // e.g., "@modal"
130
- routeName: string; // e.g., "card"
131
- handler: ReactNode | Handler<any, any>;
146
+ slotName: `@${string}`; // e.g., "@modal"
147
+ routeName: string; // e.g., "card"
148
+ handler: ReactNode | Handler<any, any, any>;
132
149
  middleware: MiddlewareFn<any, any>[];
133
150
  revalidate: ShouldRevalidateFn<any, any>[];
134
151
  errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
135
152
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
136
153
  loader: LoaderEntry[];
137
154
  loading?: ReactNode | false;
138
- layout?: ReactNode | Handler<any, any>; // Wrapper layout with <Outlet /> for content
139
- 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
140
158
  };
141
159
 
142
160
  export type EntryPropSegments = {
@@ -149,36 +167,72 @@ export type EntryPropSegments = {
149
167
  export type EntryData =
150
168
  | ({
151
169
  type: "route";
152
- handler: Handler<any, any>;
170
+ handler: Handler<any, any, any>;
153
171
  loading?: ReactNode | false;
172
+ transition?: TransitionConfig;
154
173
  /** URL pattern for this route (used by path() in urls()) */
155
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;
156
188
  } & EntryPropCommon &
157
189
  EntryPropDatas &
158
190
  EntryPropSegments)
159
191
  | ({
160
192
  type: "layout";
161
- handler: ReactNode | Handler<any, any>;
193
+ handler: ReactNode | Handler<any, any, any>;
162
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;
163
200
  } & EntryPropCommon &
164
201
  EntryPropDatas &
165
202
  EntryPropSegments)
166
203
  | ({
167
204
  type: "parallel";
168
- handler: Record<`@${string}`, Handler<any, any> | ReactNode>;
205
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
169
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>;
170
212
  } & EntryPropCommon &
171
213
  EntryPropDatas &
172
214
  EntryPropSegments)
173
215
  | ({
174
216
  type: "cache";
175
217
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
176
- handler: ReactNode | Handler<any, any>;
218
+ handler: ReactNode | Handler<any, any, any>;
177
219
  loading?: ReactNode | false;
220
+ transition?: TransitionConfig;
178
221
  } & EntryPropCommon &
179
222
  EntryPropDatas &
180
223
  EntryPropSegments);
181
224
 
225
+ /**
226
+ * Tracked include info for build-time manifest generation
227
+ */
228
+ export interface TrackedInclude {
229
+ prefix: string;
230
+ fullPrefix: string;
231
+ namePrefix?: string;
232
+ patterns: unknown; // UrlPatterns
233
+ lazy: boolean;
234
+ }
235
+
182
236
  /**
183
237
  * Context stored in AsyncLocalStorage
184
238
  */
@@ -194,17 +248,38 @@ interface HelperContext {
194
248
  isSSR?: boolean;
195
249
  /** URL patterns map for path() routes (route name -> pattern) */
196
250
  patterns?: Map<string, string>;
251
+ /** URL patterns grouped by include prefix for separate entry creation */
252
+ patternsByPrefix?: Map<string, Map<string, string>>;
197
253
  /** Trailing slash config per route name */
198
254
  trailingSlash?: Map<string, "never" | "always" | "ignore">;
255
+ /** Search param schemas per route name */
256
+ searchSchemas?: Map<string, Record<string, string>>;
199
257
  /** URL prefix from include() - applied to all path() patterns */
200
258
  urlPrefix?: string;
201
259
  /** Name prefix from include() - applied to all named routes */
202
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;
203
264
  /** Run helper for cleaner middleware code */
204
265
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
266
+ /** Tracked includes for build-time manifest generation */
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
+ >;
205
273
  }
206
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
207
- 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>());
208
283
 
209
284
  export const getContext = (): {
210
285
  context: AsyncLocalStorage<HelperContext>;
@@ -212,21 +287,21 @@ export const getContext = (): {
212
287
  getParent: () => EntryData | null;
213
288
  getOrCreateStore: (forRoute?: string) => HelperContext;
214
289
  getNextIndex: (
215
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
290
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
216
291
  ) => string;
217
292
  getShortCode: (
218
- type: "layout" | "parallel" | "route" | "loader" | "cache"
293
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
219
294
  ) => string;
220
295
  run: <T>(
221
296
  namespace: string,
222
297
  parent: EntryData | null,
223
- callback: (...args: any[]) => T
298
+ callback: (...args: any[]) => T,
224
299
  ) => T;
225
300
  runWithStore: <T>(
226
301
  store: HelperContext,
227
302
  namespace: string,
228
303
  parent: EntryData | null,
229
- callback: (...args: any[]) => T
304
+ callback: (...args: any[]) => T,
230
305
  ) => T;
231
306
  } => {
232
307
  const context = RSCRouterContext;
@@ -243,7 +318,9 @@ export const getContext = (): {
243
318
  forRoute,
244
319
  counters: {},
245
320
  patterns: new Map<string, string>(),
321
+ patternsByPrefix: new Map<string, Map<string, string>>(),
246
322
  trailingSlash: new Map<string, "never" | "always" | "ignore">(),
323
+ searchSchemas: new Map<string, Record<string, string>>(),
247
324
  } satisfies HelperContext;
248
325
  }
249
326
  return store;
@@ -252,7 +329,7 @@ export const getContext = (): {
252
329
  const store = context.getStore();
253
330
  if (!store) {
254
331
  throw new Error(
255
- "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.",
256
333
  );
257
334
  }
258
335
  return store;
@@ -266,7 +343,7 @@ export const getContext = (): {
266
343
  return store.parent;
267
344
  },
268
345
  getNextIndex: (
269
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
346
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
270
347
  ) => {
271
348
  const store = context.getStore();
272
349
  invariant(store, "No context RSCRouterContext available");
@@ -275,17 +352,31 @@ export const getContext = (): {
275
352
  store.counters[type] = index + 1;
276
353
  return `$${type}.${index}`;
277
354
  },
278
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
355
+ getShortCode: (
356
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
357
+ ) => {
279
358
  const store = context.getStore();
280
359
  invariant(store, "No context RSCRouterContext available");
281
360
 
282
361
  const parent = store.parent;
283
- const prefix = type === "layout" ? "L" : type === "parallel" ? "P" : type === "loader" ? "D" : type === "cache" ? "C" : "R";
284
- 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}` : "";
285
374
 
286
375
  if (!parent) {
287
376
  // Root entry: prefix with mount index and use mount-scoped counter
288
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
377
+ const counterKey = mountPrefix
378
+ ? `${mountPrefix}_root_${type}`
379
+ : `root_${type}`;
289
380
  store.counters[counterKey] ??= 0;
290
381
  const index = store.counters[counterKey];
291
382
  store.counters[counterKey] = index + 1;
@@ -303,7 +394,7 @@ export const getContext = (): {
303
394
  store: HelperContext,
304
395
  namespace: string,
305
396
  parent: EntryData | null,
306
- callback: (...args: any[]) => T
397
+ callback: (...args: any[]) => T,
307
398
  ): T => {
308
399
  return context.run(
309
400
  {
@@ -317,23 +408,32 @@ export const getContext = (): {
317
408
  isSSR: store.isSSR,
318
409
  patterns: store.patterns,
319
410
  trailingSlash: store.trailingSlash,
411
+ searchSchemas: store.searchSchemas,
320
412
  urlPrefix: store.urlPrefix,
321
413
  namePrefix: store.namePrefix,
414
+ rootScoped: store.rootScoped,
415
+ trackedIncludes: store.trackedIncludes,
416
+ cacheProfiles: store.cacheProfiles,
322
417
  },
323
- callback
418
+ callback,
324
419
  );
325
420
  },
326
421
  run: <T>(
327
422
  namespace: string,
328
423
  parent: EntryData | null,
329
- callback: (...args: any[]) => T
424
+ callback: (...args: any[]) => T,
330
425
  ) => {
331
426
  const store = context.getStore();
332
427
  // Preserve parent counters to ensure globally unique shortCodes
333
428
  const counters = store?.counters || {};
334
429
  const manifest = store ? store.manifest : new Map<string, EntryData>();
335
430
  const patterns = store?.patterns || new Map<string, string>();
336
- 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>>();
337
437
  return context.run(
338
438
  {
339
439
  manifest,
@@ -345,11 +445,16 @@ export const getContext = (): {
345
445
  metrics: store?.metrics,
346
446
  isSSR: store?.isSSR,
347
447
  patterns,
448
+ patternsByPrefix,
348
449
  trailingSlash,
450
+ searchSchemas,
349
451
  urlPrefix: store?.urlPrefix,
350
452
  namePrefix: store?.namePrefix,
453
+ rootScoped: store?.rootScoped,
454
+ trackedIncludes: store?.trackedIncludes,
455
+ cacheProfiles: store?.cacheProfiles,
351
456
  },
352
- callback
457
+ callback,
353
458
  );
354
459
  },
355
460
  };
@@ -362,30 +467,61 @@ export const getContext = (): {
362
467
  export function runWithPrefixes<T>(
363
468
  urlPrefix: string,
364
469
  namePrefix: string | undefined,
365
- callback: () => T
470
+ callback: () => T,
366
471
  ): T {
367
472
  const store = RSCRouterContext.getStore();
368
473
  if (!store) {
369
474
  throw new Error("runWithPrefixes must be called within router context");
370
475
  }
371
476
 
372
- // Combine prefixes if there are existing ones
373
- const combinedUrlPrefix = store.urlPrefix
374
- ? `${store.urlPrefix}${urlPrefix}`
375
- : urlPrefix;
376
- const combinedNamePrefix = namePrefix
377
- ? store.namePrefix
378
- ? `${store.namePrefix}.${namePrefix}`
379
- : namePrefix
380
- : 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;
381
516
 
382
517
  return RSCRouterContext.run(
383
518
  {
384
519
  ...store,
385
520
  urlPrefix: combinedUrlPrefix,
386
521
  namePrefix: combinedNamePrefix,
522
+ rootScoped: combinedRootScoped,
387
523
  },
388
- callback
524
+ callback,
389
525
  );
390
526
  }
391
527
 
@@ -405,6 +541,15 @@ export function getNamePrefix(): string | undefined {
405
541
  return store?.namePrefix;
406
542
  }
407
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
+
408
553
  // Export HelperContext type for use in other modules
409
554
  export type { HelperContext };
410
555
 
@@ -423,7 +568,7 @@ export type { HelperContext };
423
568
  * done(); // Records duration
424
569
  * ```
425
570
  */
426
- export function track(label: string): () => void {
571
+ export function track(label: string, depth?: number): () => void {
427
572
  const store = RSCRouterContext.getStore();
428
573
 
429
574
  // No-op if context unavailable or metrics not enabled
@@ -434,7 +579,13 @@ export function track(label: string): () => void {
434
579
  const startTime = performance.now() - store.metrics.requestStart;
435
580
 
436
581
  return () => {
437
- const duration = performance.now() - store.metrics!.requestStart - startTime;
438
- 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
+ });
439
590
  };
440
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
+ }