@rangojs/router 0.0.0-experimental.13 → 0.0.0-experimental.13221847

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 (298) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1531 -212
  4. package/dist/vite/index.js +3995 -2489
  5. package/package.json +57 -52
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -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 +6 -4
  13. package/skills/hooks/SKILL.md +328 -70
  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 +62 -15
  18. package/skills/loader/SKILL.md +368 -42
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +14 -10
  21. package/skills/parallel/SKILL.md +137 -1
  22. package/skills/prerender/SKILL.md +366 -28
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +136 -83
  25. package/skills/route/SKILL.md +195 -21
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +240 -102
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  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 +92 -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 +24 -4
  38. package/src/browser/logging.ts +11 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +266 -558
  41. package/src/browser/navigation-client.ts +132 -75
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +303 -309
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +144 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  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 +6 -1
  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 +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +188 -57
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +221 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +488 -606
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +63 -21
  82. package/src/build/generate-route-types.ts +36 -1038
  83. package/src/build/index.ts +2 -5
  84. package/src/build/route-trie.ts +38 -12
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +342 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +116 -77
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +19 -9
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +12 -7
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +104 -40
  133. package/src/index.ts +122 -67
  134. package/src/internal-debug.ts +9 -3
  135. package/src/loader.rsc.ts +18 -93
  136. package/src/loader.ts +26 -9
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +121 -17
  141. package/src/prerender.ts +325 -20
  142. package/src/reverse.ts +144 -124
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +959 -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 -1450
  151. package/src/route-map-builder.ts +87 -133
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +41 -6
  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 +160 -0
  158. package/src/router/handler-context.ts +324 -116
  159. package/src/router/intercept-resolution.ts +11 -4
  160. package/src/router/lazy-includes.ts +237 -0
  161. package/src/router/loader-resolution.ts +179 -133
  162. package/src/router/logging.ts +112 -6
  163. package/src/router/manifest.ts +58 -19
  164. package/src/router/match-api.ts +89 -88
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +86 -89
  168. package/src/router/match-middleware/cache-lookup.ts +295 -49
  169. package/src/router/match-middleware/cache-store.ts +56 -13
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -22
  171. package/src/router/match-middleware/segment-resolution.ts +20 -9
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +44 -21
  174. package/src/router/metrics.ts +240 -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 +327 -369
  178. package/src/router/pattern-matching.ts +169 -31
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +105 -14
  182. package/src/router/router-context.ts +40 -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 +677 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +199 -0
  189. package/src/router/segment-resolution/revalidation.ts +1296 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1354
  192. package/src/router/segment-wrappers.ts +291 -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 +96 -29
  197. package/src/router/types.ts +15 -9
  198. package/src/router.ts +642 -2366
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +639 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  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 +237 -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 +38 -11
  215. package/src/search-params.ts +66 -54
  216. package/src/segment-system.tsx +165 -17
  217. package/src/server/context.ts +237 -54
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +11 -6
  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 +438 -71
  223. package/src/server.ts +26 -164
  224. package/src/ssr/index.tsx +101 -31
  225. package/src/static-handler.ts +22 -4
  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 +773 -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 +109 -0
  241. package/src/types/segments.ts +150 -0
  242. package/src/types.ts +1 -1795
  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 -1323
  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 +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -2259
  261. package/src/vite/plugin-types.ts +48 -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 -47
  266. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +8 -43
  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 +266 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +181 -9
  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 -43
  289. package/dist/vite/index.named-routes.gen.ts +0 -103
  290. package/src/browser/lru-cache.ts +0 -69
  291. package/src/browser/request-controller.ts +0 -164
  292. package/src/cache/memory-store.ts +0 -253
  293. package/src/href-context.ts +0 -33
  294. package/src/router.gen.ts +0 -6
  295. package/src/static-handler.gen.ts +0 -5
  296. package/src/urls.gen.ts +0 -8
  297. package/src/vite/expose-internal-ids.ts +0 -1167
  298. /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,8 +143,8 @@ 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"
146
+ slotName: `@${string}`; // e.g., "@modal"
147
+ routeName: string; // e.g., "card"
133
148
  handler: ReactNode | Handler<any, any, any>;
134
149
  middleware: MiddlewareFn<any, any>[];
135
150
  revalidate: ShouldRevalidateFn<any, any>[];
@@ -137,14 +152,29 @@ export type InterceptEntry = {
137
152
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
138
153
  loader: LoaderEntry[];
139
154
  loading?: ReactNode | false;
140
- layout?: ReactNode | Handler<any, 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
 
@@ -153,14 +183,20 @@ export type EntryData =
153
183
  type: "route";
154
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;
158
189
  /** Set when handler is a Prerender definition */
159
190
  isPrerender?: true;
160
191
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
161
- prerenderDef?: { getParams?: () => Promise<any[]> | any[]; options?: { passthrough?: boolean } };
192
+ prerenderDef?: {
193
+ getParams?: (ctx: any) => Promise<any[]> | any[];
194
+ options?: { passthrough?: boolean };
195
+ };
162
196
  /** Set when handler is a Static definition (build-time only) */
163
197
  isStaticPrerender?: true;
198
+ /** Static handler $$id for build-time store lookup */
199
+ staticHandlerId?: string;
164
200
  /** Response type for non-RSC routes (json, text, image, any) */
165
201
  responseType?: string;
166
202
  } & EntryPropCommon &
@@ -170,25 +206,21 @@ export type EntryData =
170
206
  type: "layout";
171
207
  handler: ReactNode | Handler<any, any, any>;
172
208
  loading?: ReactNode | false;
209
+ transition?: TransitionConfig;
173
210
  /** Set when handler is a Static definition (build-time only) */
174
211
  isStaticPrerender?: true;
212
+ /** Static handler $$id for build-time store lookup */
213
+ staticHandlerId?: string;
175
214
  } & EntryPropCommon &
176
215
  EntryPropDatas &
177
216
  EntryPropSegments)
178
- | ({
179
- type: "parallel";
180
- handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
181
- loading?: ReactNode | false;
182
- /** Set when any parallel slot is a Static definition */
183
- isStaticPrerender?: true;
184
- } & EntryPropCommon &
185
- EntryPropDatas &
186
- EntryPropSegments)
217
+ | ParallelEntryData
187
218
  | ({
188
219
  type: "cache";
189
220
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
190
221
  handler: ReactNode | Handler<any, any, any>;
191
222
  loading?: ReactNode | false;
223
+ transition?: TransitionConfig;
192
224
  } & EntryPropCommon &
193
225
  EntryPropDatas &
194
226
  EntryPropSegments);
@@ -229,13 +261,28 @@ interface HelperContext {
229
261
  urlPrefix?: string;
230
262
  /** Name prefix from include() - applied to all named routes */
231
263
  namePrefix?: string;
264
+ /** True when this scope is at root level (no named include boundary above).
265
+ * Routes at root scope allow dot-local reverse to fall back to bare names. */
266
+ rootScoped?: boolean;
232
267
  /** Run helper for cleaner middleware code */
233
268
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
234
269
  /** Tracked includes for build-time manifest generation */
235
270
  trackedIncludes?: TrackedInclude[];
271
+ /** Cache profiles for DSL-time cache("profileName") resolution */
272
+ cacheProfiles?: Record<
273
+ string,
274
+ import("../cache/profile-registry.js").CacheProfile
275
+ >;
236
276
  }
237
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
238
- new AsyncLocalStorage<HelperContext>();
277
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
278
+ // module re-evaluation. Without this, Vite's RSC module runner may create
279
+ // a new instance when context.ts is re-evaluated, while other modules still
280
+ // hold references to the old instance — causing getStore() to return
281
+ // undefined even inside a run() callback.
282
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
283
+ export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
284
+ globalThis as any
285
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
239
286
 
240
287
  export const getContext = (): {
241
288
  context: AsyncLocalStorage<HelperContext>;
@@ -243,21 +290,21 @@ export const getContext = (): {
243
290
  getParent: () => EntryData | null;
244
291
  getOrCreateStore: (forRoute?: string) => HelperContext;
245
292
  getNextIndex: (
246
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
293
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
247
294
  ) => string;
248
295
  getShortCode: (
249
- type: "layout" | "parallel" | "route" | "loader" | "cache"
296
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
250
297
  ) => string;
251
298
  run: <T>(
252
299
  namespace: string,
253
300
  parent: EntryData | null,
254
- callback: (...args: any[]) => T
301
+ callback: (...args: any[]) => T,
255
302
  ) => T;
256
303
  runWithStore: <T>(
257
304
  store: HelperContext,
258
305
  namespace: string,
259
306
  parent: EntryData | null,
260
- callback: (...args: any[]) => T
307
+ callback: (...args: any[]) => T,
261
308
  ) => T;
262
309
  } => {
263
310
  const context = RSCRouterContext;
@@ -285,7 +332,7 @@ export const getContext = (): {
285
332
  const store = context.getStore();
286
333
  if (!store) {
287
334
  throw new Error(
288
- "RSC Router context store is not available. Make sure to run within RSC Router context."
335
+ "RSC Router context store is not available. Make sure to run within RSC Router context.",
289
336
  );
290
337
  }
291
338
  return store;
@@ -299,7 +346,7 @@ export const getContext = (): {
299
346
  return store.parent;
300
347
  },
301
348
  getNextIndex: (
302
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
349
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
303
350
  ) => {
304
351
  const store = context.getStore();
305
352
  invariant(store, "No context RSCRouterContext available");
@@ -308,17 +355,31 @@ export const getContext = (): {
308
355
  store.counters[type] = index + 1;
309
356
  return `$${type}.${index}`;
310
357
  },
311
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
358
+ getShortCode: (
359
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
360
+ ) => {
312
361
  const store = context.getStore();
313
362
  invariant(store, "No context RSCRouterContext available");
314
363
 
315
364
  const parent = store.parent;
316
- const prefix = type === "layout" ? "L" : type === "parallel" ? "P" : type === "loader" ? "D" : type === "cache" ? "C" : "R";
317
- const mountPrefix = store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
365
+ const prefix =
366
+ type === "layout"
367
+ ? "L"
368
+ : type === "parallel"
369
+ ? "P"
370
+ : type === "loader"
371
+ ? "D"
372
+ : type === "cache"
373
+ ? "C"
374
+ : "R";
375
+ const mountPrefix =
376
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
318
377
 
319
378
  if (!parent) {
320
379
  // Root entry: prefix with mount index and use mount-scoped counter
321
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
380
+ const counterKey = mountPrefix
381
+ ? `${mountPrefix}_root_${type}`
382
+ : `root_${type}`;
322
383
  store.counters[counterKey] ??= 0;
323
384
  const index = store.counters[counterKey];
324
385
  store.counters[counterKey] = index + 1;
@@ -336,7 +397,7 @@ export const getContext = (): {
336
397
  store: HelperContext,
337
398
  namespace: string,
338
399
  parent: EntryData | null,
339
- callback: (...args: any[]) => T
400
+ callback: (...args: any[]) => T,
340
401
  ): T => {
341
402
  return context.run(
342
403
  {
@@ -353,23 +414,29 @@ export const getContext = (): {
353
414
  searchSchemas: store.searchSchemas,
354
415
  urlPrefix: store.urlPrefix,
355
416
  namePrefix: store.namePrefix,
417
+ rootScoped: store.rootScoped,
356
418
  trackedIncludes: store.trackedIncludes,
419
+ cacheProfiles: store.cacheProfiles,
357
420
  },
358
- callback
421
+ callback,
359
422
  );
360
423
  },
361
424
  run: <T>(
362
425
  namespace: string,
363
426
  parent: EntryData | null,
364
- callback: (...args: any[]) => T
427
+ callback: (...args: any[]) => T,
365
428
  ) => {
366
429
  const store = context.getStore();
367
430
  // Preserve parent counters to ensure globally unique shortCodes
368
431
  const counters = store?.counters || {};
369
432
  const manifest = store ? store.manifest : new Map<string, EntryData>();
370
433
  const patterns = store?.patterns || new Map<string, string>();
371
- const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
372
- const searchSchemas = store?.searchSchemas || new Map<string, Record<string, string>>();
434
+ const patternsByPrefix = store?.patternsByPrefix;
435
+ const trailingSlash =
436
+ store?.trailingSlash ||
437
+ new Map<string, "never" | "always" | "ignore">();
438
+ const searchSchemas =
439
+ store?.searchSchemas || new Map<string, Record<string, string>>();
373
440
  return context.run(
374
441
  {
375
442
  manifest,
@@ -381,13 +448,16 @@ export const getContext = (): {
381
448
  metrics: store?.metrics,
382
449
  isSSR: store?.isSSR,
383
450
  patterns,
451
+ patternsByPrefix,
384
452
  trailingSlash,
385
453
  searchSchemas,
386
454
  urlPrefix: store?.urlPrefix,
387
455
  namePrefix: store?.namePrefix,
456
+ rootScoped: store?.rootScoped,
388
457
  trackedIncludes: store?.trackedIncludes,
458
+ cacheProfiles: store?.cacheProfiles,
389
459
  },
390
- callback
460
+ callback,
391
461
  );
392
462
  },
393
463
  };
@@ -400,7 +470,7 @@ export const getContext = (): {
400
470
  export function runWithPrefixes<T>(
401
471
  urlPrefix: string,
402
472
  namePrefix: string | undefined,
403
- callback: () => T
473
+ callback: () => T,
404
474
  ): T {
405
475
  const store = RSCRouterContext.getStore();
406
476
  if (!store) {
@@ -418,19 +488,43 @@ export function runWithPrefixes<T>(
418
488
  } else {
419
489
  combinedUrlPrefix = urlPrefix;
420
490
  }
421
- const combinedNamePrefix = namePrefix
422
- ? store.namePrefix
423
- ? `${store.namePrefix}.${namePrefix}`
424
- : namePrefix
425
- : store.namePrefix;
491
+ const combinedNamePrefix =
492
+ namePrefix !== undefined
493
+ ? namePrefix === ""
494
+ ? store.namePrefix
495
+ : store.namePrefix
496
+ ? `${store.namePrefix}.${namePrefix}`
497
+ : namePrefix
498
+ : store.namePrefix;
499
+
500
+ // Track root scope for dot-local reverse resolution.
501
+ //
502
+ // The flag answers: "can this route reach bare names at root scope?"
503
+ // It propagates through the include chain:
504
+ //
505
+ // { name: "" } — transparent: inherit parent, default true
506
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
507
+ // no name — inherit parent unchanged
508
+ //
509
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
510
+ // (the outer transparent include establishes root access, and the inner
511
+ // named include inherits it). But a direct { name: "sub" } at root gets
512
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
513
+ const combinedRootScoped =
514
+ namePrefix === ""
515
+ ? (store.rootScoped ?? true)
516
+ : namePrefix !== undefined
517
+ ? (store.rootScoped ?? false)
518
+ : store.rootScoped;
426
519
 
427
520
  return RSCRouterContext.run(
428
521
  {
429
522
  ...store,
430
523
  urlPrefix: combinedUrlPrefix,
431
524
  namePrefix: combinedNamePrefix,
525
+ rootScoped: combinedRootScoped,
432
526
  },
433
- callback
527
+ callback,
434
528
  );
435
529
  }
436
530
 
@@ -450,9 +544,92 @@ export function getNamePrefix(): string | undefined {
450
544
  return store?.namePrefix;
451
545
  }
452
546
 
547
+ /**
548
+ * Get whether the current scope is at root level (no named include boundary above).
549
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
550
+ */
551
+ export function getRootScoped(): boolean {
552
+ const store = RSCRouterContext.getStore();
553
+ return store?.rootScoped ?? true;
554
+ }
555
+
453
556
  // Export HelperContext type for use in other modules
454
557
  export type { HelperContext };
455
558
 
559
+ /**
560
+ * Return an isolated copy of a lazy include's captured parent entry.
561
+ *
562
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
563
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
564
+ * parent, so without isolation one include's loaders/middleware leak into
565
+ * every other route that shares that root.
566
+ *
567
+ * The clone is shallow: only the mutable arrays are copied so each
568
+ * include pushes to its own list. The rest of the entry (id, shortCode,
569
+ * parent pointer, handler) stays shared, which is correct and cheap.
570
+ */
571
+ export function getIsolatedLazyParent(
572
+ captured: EntryData | null | undefined,
573
+ ): EntryData | null {
574
+ if (!captured) return null;
575
+ return {
576
+ ...captured,
577
+ loader: [...captured.loader],
578
+ middleware: [...captured.middleware],
579
+ revalidate: [...captured.revalidate],
580
+ errorBoundary: [...captured.errorBoundary],
581
+ notFoundBoundary: [...captured.notFoundBoundary],
582
+ layout: [...captured.layout],
583
+ parallel: { ...captured.parallel },
584
+ intercept: [...captured.intercept],
585
+ };
586
+ }
587
+
588
+ export function getParallelEntries(
589
+ parallels: ParallelEntries | EntryData[] | undefined,
590
+ ): ParallelEntryData[] {
591
+ if (!parallels) return [];
592
+ if (Array.isArray(parallels)) {
593
+ return parallels.filter(
594
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
595
+ );
596
+ }
597
+ return Object.values(parallels).filter(
598
+ (entry): entry is ParallelEntryData => !!entry,
599
+ );
600
+ }
601
+
602
+ export function getParallelSlotEntries(
603
+ parallels: ParallelEntries | EntryData[] | undefined,
604
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
605
+ if (!parallels) return [];
606
+
607
+ if (Array.isArray(parallels)) {
608
+ return getParallelEntries(parallels).flatMap((entry) =>
609
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
610
+ slot,
611
+ entry,
612
+ })),
613
+ );
614
+ }
615
+
616
+ return Object.entries(parallels)
617
+ .filter(([, entry]) => !!entry)
618
+ .map(([slot, entry]) => ({
619
+ slot: slot as `@${string}`,
620
+ entry: entry!,
621
+ }));
622
+ }
623
+
624
+ export function getParallelSlotCount(
625
+ parallels: ParallelEntries | EntryData[] | undefined,
626
+ ): number {
627
+ if (!parallels) return 0;
628
+ return Array.isArray(parallels)
629
+ ? parallels.filter((entry) => entry?.type === "parallel").length
630
+ : Object.keys(parallels).length;
631
+ }
632
+
456
633
  // ============================================================================
457
634
  // Performance Metrics Helpers
458
635
  // ============================================================================
@@ -468,7 +645,7 @@ export type { HelperContext };
468
645
  * done(); // Records duration
469
646
  * ```
470
647
  */
471
- export function track(label: string): () => void {
648
+ export function track(label: string, depth?: number): () => void {
472
649
  const store = RSCRouterContext.getStore();
473
650
 
474
651
  // No-op if context unavailable or metrics not enabled
@@ -479,7 +656,13 @@ export function track(label: string): () => void {
479
656
  const startTime = performance.now() - store.metrics.requestStart;
480
657
 
481
658
  return () => {
482
- const duration = performance.now() - store.metrics!.requestStart - startTime;
483
- store.metrics!.metrics.push({ label, duration, startTime });
659
+ const duration =
660
+ performance.now() - store.metrics!.requestStart - startTime;
661
+ store.metrics!.metrics.push({
662
+ label,
663
+ duration,
664
+ startTime,
665
+ ...(depth != null ? { depth } : {}),
666
+ });
484
667
  };
485
668
  }
@@ -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
+ }