@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100

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 (329) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +1037 -4
  3. package/dist/bin/rango.js +1619 -157
  4. package/dist/vite/index.js +5762 -2301
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +71 -63
  7. package/skills/breadcrumbs/SKILL.md +252 -0
  8. package/skills/cache-guide/SKILL.md +294 -0
  9. package/skills/caching/SKILL.md +93 -23
  10. package/skills/composability/SKILL.md +172 -0
  11. package/skills/debug-manifest/SKILL.md +12 -8
  12. package/skills/document-cache/SKILL.md +18 -16
  13. package/skills/fonts/SKILL.md +6 -4
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +367 -71
  16. package/skills/host-router/SKILL.md +218 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +176 -8
  19. package/skills/layout/SKILL.md +124 -3
  20. package/skills/links/SKILL.md +304 -25
  21. package/skills/loader/SKILL.md +474 -47
  22. package/skills/middleware/SKILL.md +207 -37
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +15 -11
  26. package/skills/parallel/SKILL.md +272 -1
  27. package/skills/prerender/SKILL.md +467 -65
  28. package/skills/rango/SKILL.md +89 -21
  29. package/skills/response-routes/SKILL.md +152 -91
  30. package/skills/route/SKILL.md +305 -14
  31. package/skills/router-setup/SKILL.md +210 -32
  32. package/skills/server-actions/SKILL.md +739 -0
  33. package/skills/streams-and-websockets/SKILL.md +283 -0
  34. package/skills/theme/SKILL.md +9 -8
  35. package/skills/typesafety/SKILL.md +333 -86
  36. package/skills/use-cache/SKILL.md +324 -0
  37. package/skills/view-transitions/SKILL.md +212 -0
  38. package/src/__internal.ts +102 -4
  39. package/src/bin/rango.ts +312 -15
  40. package/src/browser/action-coordinator.ts +97 -0
  41. package/src/browser/action-response-classifier.ts +99 -0
  42. package/src/browser/app-shell.ts +52 -0
  43. package/src/browser/app-version.ts +14 -0
  44. package/src/browser/event-controller.ts +136 -68
  45. package/src/browser/history-state.ts +80 -0
  46. package/src/browser/intercept-utils.ts +52 -0
  47. package/src/browser/link-interceptor.ts +24 -4
  48. package/src/browser/logging.ts +55 -0
  49. package/src/browser/merge-segment-loaders.ts +20 -12
  50. package/src/browser/navigation-bridge.ts +374 -561
  51. package/src/browser/navigation-client.ts +228 -70
  52. package/src/browser/navigation-store.ts +97 -55
  53. package/src/browser/navigation-transaction.ts +297 -0
  54. package/src/browser/network-error-handler.ts +61 -0
  55. package/src/browser/partial-update.ts +376 -315
  56. package/src/browser/prefetch/cache.ts +314 -0
  57. package/src/browser/prefetch/fetch.ts +282 -0
  58. package/src/browser/prefetch/observer.ts +65 -0
  59. package/src/browser/prefetch/policy.ts +48 -0
  60. package/src/browser/prefetch/queue.ts +191 -0
  61. package/src/browser/prefetch/resource-ready.ts +77 -0
  62. package/src/browser/rango-state.ts +152 -0
  63. package/src/browser/react/Link.tsx +255 -71
  64. package/src/browser/react/NavigationProvider.tsx +152 -24
  65. package/src/browser/react/context.ts +11 -0
  66. package/src/browser/react/filter-segment-order.ts +55 -0
  67. package/src/browser/react/index.ts +15 -12
  68. package/src/browser/react/location-state-shared.ts +95 -53
  69. package/src/browser/react/location-state.ts +60 -15
  70. package/src/browser/react/mount-context.ts +6 -1
  71. package/src/browser/react/nonce-context.ts +23 -0
  72. package/src/browser/react/shallow-equal.ts +27 -0
  73. package/src/browser/react/use-action.ts +29 -51
  74. package/src/browser/react/use-client-cache.ts +5 -3
  75. package/src/browser/react/use-handle.ts +30 -120
  76. package/src/browser/react/use-link-status.ts +6 -5
  77. package/src/browser/react/use-navigation.ts +44 -65
  78. package/src/browser/react/use-params.ts +78 -0
  79. package/src/browser/react/use-pathname.ts +47 -0
  80. package/src/browser/react/use-reverse.ts +99 -0
  81. package/src/browser/react/use-router.ts +83 -0
  82. package/src/browser/react/use-search-params.ts +56 -0
  83. package/src/browser/react/use-segments.ts +85 -99
  84. package/src/browser/response-adapter.ts +73 -0
  85. package/src/browser/rsc-router.tsx +246 -64
  86. package/src/browser/scroll-restoration.ts +127 -52
  87. package/src/browser/segment-reconciler.ts +243 -0
  88. package/src/browser/segment-structure-assert.ts +16 -0
  89. package/src/browser/server-action-bridge.ts +510 -603
  90. package/src/browser/shallow.ts +6 -1
  91. package/src/browser/types.ts +158 -48
  92. package/src/browser/validate-redirect-origin.ts +29 -0
  93. package/src/build/generate-manifest.ts +84 -23
  94. package/src/build/generate-route-types.ts +39 -828
  95. package/src/build/index.ts +4 -5
  96. package/src/build/route-trie.ts +85 -32
  97. package/src/build/route-types/ast-helpers.ts +25 -0
  98. package/src/build/route-types/ast-route-extraction.ts +98 -0
  99. package/src/build/route-types/codegen.ts +102 -0
  100. package/src/build/route-types/include-resolution.ts +418 -0
  101. package/src/build/route-types/param-extraction.ts +48 -0
  102. package/src/build/route-types/per-module-writer.ts +128 -0
  103. package/src/build/route-types/router-processing.ts +618 -0
  104. package/src/build/route-types/scan-filter.ts +85 -0
  105. package/src/build/runtime-discovery.ts +231 -0
  106. package/src/cache/background-task.ts +34 -0
  107. package/src/cache/cache-key-utils.ts +44 -0
  108. package/src/cache/cache-policy.ts +125 -0
  109. package/src/cache/cache-runtime.ts +342 -0
  110. package/src/cache/cache-scope.ts +167 -307
  111. package/src/cache/cf/cf-cache-store.ts +573 -21
  112. package/src/cache/cf/index.ts +13 -3
  113. package/src/cache/document-cache.ts +116 -77
  114. package/src/cache/handle-capture.ts +81 -0
  115. package/src/cache/handle-snapshot.ts +41 -0
  116. package/src/cache/index.ts +1 -15
  117. package/src/cache/memory-segment-store.ts +191 -13
  118. package/src/cache/profile-registry.ts +73 -0
  119. package/src/cache/read-through-swr.ts +134 -0
  120. package/src/cache/segment-codec.ts +256 -0
  121. package/src/cache/taint.ts +153 -0
  122. package/src/cache/types.ts +72 -122
  123. package/src/client.rsc.tsx +6 -1
  124. package/src/client.tsx +118 -302
  125. package/src/component-utils.ts +4 -4
  126. package/src/components/DefaultDocument.tsx +5 -1
  127. package/src/context-var.ts +156 -0
  128. package/src/debug.ts +19 -9
  129. package/src/errors.ts +77 -7
  130. package/src/handle.ts +55 -10
  131. package/src/handles/MetaTags.tsx +73 -20
  132. package/src/handles/breadcrumbs.ts +66 -0
  133. package/src/handles/index.ts +1 -0
  134. package/src/handles/meta.ts +30 -13
  135. package/src/host/cookie-handler.ts +21 -15
  136. package/src/host/errors.ts +8 -8
  137. package/src/host/index.ts +4 -7
  138. package/src/host/pattern-matcher.ts +27 -27
  139. package/src/host/router.ts +61 -39
  140. package/src/host/testing.ts +8 -8
  141. package/src/host/types.ts +15 -7
  142. package/src/host/utils.ts +1 -1
  143. package/src/href-client.ts +65 -45
  144. package/src/index.rsc.ts +138 -21
  145. package/src/index.ts +206 -51
  146. package/src/internal-debug.ts +11 -0
  147. package/src/loader.rsc.ts +25 -143
  148. package/src/loader.ts +27 -10
  149. package/src/network-error-thrower.tsx +3 -1
  150. package/src/outlet-context.ts +1 -1
  151. package/src/outlet-provider.tsx +45 -0
  152. package/src/prerender/param-hash.ts +4 -2
  153. package/src/prerender/store.ts +159 -13
  154. package/src/prerender.ts +397 -29
  155. package/src/response-utils.ts +28 -0
  156. package/src/reverse.ts +231 -121
  157. package/src/root-error-boundary.tsx +41 -29
  158. package/src/route-content-wrapper.tsx +7 -4
  159. package/src/route-definition/dsl-helpers.ts +1134 -0
  160. package/src/route-definition/helper-factories.ts +200 -0
  161. package/src/route-definition/helpers-types.ts +483 -0
  162. package/src/route-definition/index.ts +55 -0
  163. package/src/route-definition/redirect.ts +101 -0
  164. package/src/route-definition/resolve-handler-use.ts +155 -0
  165. package/src/route-definition.ts +1 -1431
  166. package/src/route-map-builder.ts +162 -123
  167. package/src/route-name.ts +53 -0
  168. package/src/route-types.ts +66 -9
  169. package/src/router/content-negotiation.ts +215 -0
  170. package/src/router/debug-manifest.ts +72 -0
  171. package/src/router/error-handling.ts +9 -9
  172. package/src/router/find-match.ts +160 -0
  173. package/src/router/handler-context.ts +418 -86
  174. package/src/router/intercept-resolution.ts +35 -20
  175. package/src/router/lazy-includes.ts +237 -0
  176. package/src/router/loader-resolution.ts +359 -128
  177. package/src/router/logging.ts +251 -0
  178. package/src/router/manifest.ts +98 -32
  179. package/src/router/match-api.ts +196 -261
  180. package/src/router/match-context.ts +4 -2
  181. package/src/router/match-handlers.ts +441 -0
  182. package/src/router/match-middleware/background-revalidation.ts +108 -93
  183. package/src/router/match-middleware/cache-lookup.ts +415 -86
  184. package/src/router/match-middleware/cache-store.ts +91 -29
  185. package/src/router/match-middleware/intercept-resolution.ts +48 -21
  186. package/src/router/match-middleware/segment-resolution.ts +73 -9
  187. package/src/router/match-pipelines.ts +10 -45
  188. package/src/router/match-result.ts +154 -35
  189. package/src/router/metrics.ts +240 -15
  190. package/src/router/middleware-cookies.ts +55 -0
  191. package/src/router/middleware-types.ts +209 -0
  192. package/src/router/middleware.ts +373 -371
  193. package/src/router/navigation-snapshot.ts +182 -0
  194. package/src/router/pattern-matching.ts +292 -52
  195. package/src/router/prerender-match.ts +502 -0
  196. package/src/router/preview-match.ts +98 -0
  197. package/src/router/request-classification.ts +310 -0
  198. package/src/router/revalidation.ts +152 -39
  199. package/src/router/route-snapshot.ts +245 -0
  200. package/src/router/router-context.ts +41 -21
  201. package/src/router/router-interfaces.ts +484 -0
  202. package/src/router/router-options.ts +618 -0
  203. package/src/router/router-registry.ts +24 -0
  204. package/src/router/segment-resolution/fresh.ts +756 -0
  205. package/src/router/segment-resolution/helpers.ts +268 -0
  206. package/src/router/segment-resolution/loader-cache.ts +199 -0
  207. package/src/router/segment-resolution/revalidation.ts +1407 -0
  208. package/src/router/segment-resolution/static-store.ts +67 -0
  209. package/src/router/segment-resolution.ts +21 -1315
  210. package/src/router/segment-wrappers.ts +291 -0
  211. package/src/router/substitute-pattern-params.ts +56 -0
  212. package/src/router/telemetry-otel.ts +299 -0
  213. package/src/router/telemetry.ts +300 -0
  214. package/src/router/timeout.ts +148 -0
  215. package/src/router/trie-matching.ts +111 -39
  216. package/src/router/types.ts +17 -9
  217. package/src/router/url-params.ts +49 -0
  218. package/src/router.ts +642 -2011
  219. package/src/rsc/handler-context.ts +45 -0
  220. package/src/rsc/handler.ts +864 -1114
  221. package/src/rsc/helpers.ts +181 -19
  222. package/src/rsc/index.ts +0 -20
  223. package/src/rsc/loader-fetch.ts +229 -0
  224. package/src/rsc/manifest-init.ts +90 -0
  225. package/src/rsc/nonce.ts +14 -0
  226. package/src/rsc/origin-guard.ts +141 -0
  227. package/src/rsc/progressive-enhancement.ts +395 -0
  228. package/src/rsc/response-error.ts +37 -0
  229. package/src/rsc/response-route-handler.ts +360 -0
  230. package/src/rsc/rsc-rendering.ts +256 -0
  231. package/src/rsc/runtime-warnings.ts +42 -0
  232. package/src/rsc/server-action.ts +360 -0
  233. package/src/rsc/ssr-setup.ts +128 -0
  234. package/src/rsc/types.ts +52 -11
  235. package/src/search-params.ts +230 -0
  236. package/src/segment-content-promise.ts +67 -0
  237. package/src/segment-loader-promise.ts +122 -0
  238. package/src/segment-system.tsx +187 -38
  239. package/src/server/context.ts +333 -59
  240. package/src/server/cookie-store.ts +190 -0
  241. package/src/server/fetchable-loader-store.ts +37 -0
  242. package/src/server/handle-store.ts +113 -15
  243. package/src/server/loader-registry.ts +24 -64
  244. package/src/server/request-context.ts +603 -109
  245. package/src/server.ts +35 -155
  246. package/src/ssr/index.tsx +107 -30
  247. package/src/static-handler.ts +126 -0
  248. package/src/theme/ThemeProvider.tsx +21 -15
  249. package/src/theme/ThemeScript.tsx +5 -5
  250. package/src/theme/constants.ts +5 -2
  251. package/src/theme/index.ts +4 -14
  252. package/src/theme/theme-context.ts +4 -30
  253. package/src/theme/theme-script.ts +21 -18
  254. package/src/types/boundaries.ts +158 -0
  255. package/src/types/cache-types.ts +198 -0
  256. package/src/types/error-types.ts +192 -0
  257. package/src/types/global-namespace.ts +100 -0
  258. package/src/types/handler-context.ts +764 -0
  259. package/src/types/index.ts +88 -0
  260. package/src/types/loader-types.ts +209 -0
  261. package/src/types/request-scope.ts +126 -0
  262. package/src/types/route-config.ts +170 -0
  263. package/src/types/route-entry.ts +120 -0
  264. package/src/types/segments.ts +167 -0
  265. package/src/types.ts +1 -1757
  266. package/src/urls/include-helper.ts +207 -0
  267. package/src/urls/index.ts +53 -0
  268. package/src/urls/path-helper-types.ts +372 -0
  269. package/src/urls/path-helper.ts +364 -0
  270. package/src/urls/pattern-types.ts +107 -0
  271. package/src/urls/response-types.ts +108 -0
  272. package/src/urls/type-extraction.ts +372 -0
  273. package/src/urls/urls-function.ts +98 -0
  274. package/src/urls.ts +1 -1282
  275. package/src/use-loader.tsx +161 -81
  276. package/src/vite/debug.ts +184 -0
  277. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  278. package/src/vite/discovery/discover-routers.ts +376 -0
  279. package/src/vite/discovery/gate-state.ts +171 -0
  280. package/src/vite/discovery/prerender-collection.ts +486 -0
  281. package/src/vite/discovery/route-types-writer.ts +258 -0
  282. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  283. package/src/vite/discovery/state.ts +117 -0
  284. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  285. package/src/vite/index.ts +15 -2063
  286. package/src/vite/plugin-types.ts +103 -0
  287. package/src/vite/plugins/cjs-to-esm.ts +98 -0
  288. package/src/vite/plugins/client-ref-dedup.ts +131 -0
  289. package/src/vite/plugins/client-ref-hashing.ts +117 -0
  290. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  291. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  292. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  293. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
  294. package/src/vite/plugins/expose-id-utils.ts +299 -0
  295. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  296. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  297. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  298. package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
  299. package/src/vite/plugins/expose-ids/types.ts +45 -0
  300. package/src/vite/plugins/expose-internal-ids.ts +816 -0
  301. package/src/vite/plugins/performance-tracks.ts +96 -0
  302. package/src/vite/plugins/refresh-cmd.ts +127 -0
  303. package/src/vite/plugins/use-cache-transform.ts +336 -0
  304. package/src/vite/plugins/version-injector.ts +109 -0
  305. package/src/vite/plugins/version-plugin.ts +266 -0
  306. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  307. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  308. package/src/vite/rango.ts +497 -0
  309. package/src/vite/router-discovery.ts +1423 -0
  310. package/src/vite/utils/ast-handler-extract.ts +517 -0
  311. package/src/vite/utils/banner.ts +36 -0
  312. package/src/vite/utils/bundle-analysis.ts +137 -0
  313. package/src/vite/utils/manifest-utils.ts +70 -0
  314. package/src/vite/utils/package-resolution.ts +161 -0
  315. package/src/vite/utils/prerender-utils.ts +222 -0
  316. package/src/vite/utils/shared-utils.ts +170 -0
  317. package/CLAUDE.md +0 -43
  318. package/src/browser/lru-cache.ts +0 -69
  319. package/src/browser/request-controller.ts +0 -164
  320. package/src/cache/memory-store.ts +0 -253
  321. package/src/href-context.ts +0 -33
  322. package/src/router.gen.ts +0 -6
  323. package/src/urls.gen.ts +0 -8
  324. package/src/vite/expose-handle-id.ts +0 -209
  325. package/src/vite/expose-loader-id.ts +0 -426
  326. package/src/vite/expose-location-state-id.ts +0 -177
  327. package/src/vite/expose-prerender-handler-id.ts +0 -429
  328. package/src/vite/package-resolution.ts +0 -125
  329. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -17,15 +17,12 @@
17
17
 
18
18
  export {
19
19
  generateManifest,
20
+ generateManifestFull,
20
21
  generateManifestCode,
21
22
  type GeneratedManifest,
22
23
  } from "./generate-manifest.js";
23
24
 
24
- export {
25
- buildRouteTrie,
26
- type TrieNode,
27
- type TrieLeaf,
28
- } from "./route-trie.js";
25
+ export { buildRouteTrie, type TrieNode, type TrieLeaf } from "./route-trie.js";
29
26
 
30
27
  export {
31
28
  writePerModuleRouteTypes,
@@ -34,3 +31,5 @@ export {
34
31
  createScanFilter,
35
32
  type ScanFilter,
36
33
  } from "./generate-route-types.js";
34
+
35
+ export { hashParams } from "../prerender/param-hash.js";
@@ -6,7 +6,10 @@
6
6
  * shortCodes for layout pruning.
7
7
  */
8
8
 
9
- import { parsePattern, type ParsedSegment } from "../router/pattern-matching.js";
9
+ import {
10
+ parsePattern,
11
+ type ParsedSegment,
12
+ } from "../router/pattern-matching.js";
10
13
 
11
14
  // -- Trie data structures (compact keys for JSON serialization) --
12
15
 
@@ -17,10 +20,13 @@ export interface TrieLeaf {
17
20
  sp: string;
18
21
  /** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
19
22
  a: string[];
20
- /** Optional param names (absent params get empty string value) */
23
+ /** Optional param names declared on the route. Absent params are
24
+ * omitted from the matched params record (read as `undefined`). */
21
25
  op?: string[];
22
26
  /** Constraint validation: paramName -> allowed values */
23
27
  cv?: Record<string, string[]>;
28
+ /** Ordered param names for this route (positional) */
29
+ pa?: string[];
24
30
  /** Trailing slash mode */
25
31
  ts?: string;
26
32
  /** Route has pre-rendered data available */
@@ -42,6 +48,8 @@ export interface TrieNode {
42
48
  s?: Record<string, TrieNode>;
43
49
  /** Param child: { n: paramName, c: child node } */
44
50
  p?: { n: string; c: TrieNode };
51
+ /** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
52
+ xp?: Record<string, { n: string; c: TrieNode }>;
45
53
  /** Wildcard terminal: leaf + paramName */
46
54
  w?: TrieLeaf & { pn: string };
47
55
  }
@@ -91,42 +99,46 @@ export function buildRouteTrie(
91
99
  }
92
100
 
93
101
  /**
94
- * Insert a route into the trie, handling optional params by forking
95
- * the insertion path (one terminal without the param, one with).
102
+ * Insert a route into the trie. Optional params expand into two branches at
103
+ * registration time (skip-first, then present), so each terminal lives at the
104
+ * correct depth for its number of bound params and carries a branch-local
105
+ * `pa` listing only those names. The trie's single-slot `node.p` is reused
106
+ * across branches because matching ignores `node.p.n` — the leaf's `pa` is
107
+ * the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
108
+ * last-wins rule produce greedy-leftmost semantics for free at any shared
109
+ * terminal depth.
96
110
  */
97
111
  function insertRoute(
98
112
  node: TrieNode,
99
113
  segments: ParsedSegment[],
100
114
  index: number,
101
- leaf: Omit<TrieLeaf, "op" | "cv">,
115
+ leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
102
116
  ): void {
103
- // Collect optional param names and constraints across all segments
117
+ // op (full optional list) and cv (full constraint map) are route-level and
118
+ // identical on every terminal, so compute them once on the shared base.
104
119
  const optionalParams: string[] = [];
105
120
  const constraints: Record<string, string[]> = {};
106
121
 
107
122
  for (const seg of segments) {
108
- if (seg.type === "param" && seg.optional) {
109
- optionalParams.push(seg.value);
110
- }
111
- if (seg.type === "param" && seg.constraint) {
112
- constraints[seg.value] = seg.constraint;
123
+ if (seg.type === "param") {
124
+ if (seg.optional) {
125
+ optionalParams.push(seg.value);
126
+ }
127
+ if (seg.constraint) {
128
+ constraints[seg.value] = seg.constraint;
129
+ }
113
130
  }
114
131
  }
115
132
 
116
- const fullLeaf: TrieLeaf = {
133
+ const leafBase: Omit<TrieLeaf, "pa"> = {
117
134
  ...leaf,
118
135
  ...(optionalParams.length > 0 ? { op: optionalParams } : {}),
119
136
  ...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
120
137
  };
121
138
 
122
- insertSegments(node, segments, index, fullLeaf);
139
+ insertSegments(node, segments, index, leafBase, []);
123
140
  }
124
141
 
125
- /**
126
- * Recursively insert segments into the trie.
127
- * For optional params, we add a terminal at the current node (param absent)
128
- * AND continue inserting into the param child (param present).
129
- */
130
142
  /**
131
143
  * Extract ancestry map from a built trie by visiting all leaf nodes.
132
144
  * Returns { routeName: ancestryShortCodes[] } for every route in the trie.
@@ -148,6 +160,11 @@ export function extractAncestryFromTrie(
148
160
  visit(child);
149
161
  }
150
162
  }
163
+ if (node.xp) {
164
+ for (const child of Object.values(node.xp)) {
165
+ visit(child.c);
166
+ }
167
+ }
151
168
  if (node.p) {
152
169
  visit(node.p.c);
153
170
  }
@@ -188,8 +205,8 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
188
205
  // Response-type was primary, new leaf is RSC: swap and move old to variants
189
206
  // RSC was defined second (response-type was already the existing leaf)
190
207
  if (!leaf.nv) leaf.nv = [];
191
- leaf.nv.push({ routeKey: existing.n, responseType: existing.rt });
192
208
  if (existing.nv) leaf.nv.push(...existing.nv);
209
+ leaf.nv.push({ routeKey: existing.n, responseType: existing.rt });
193
210
  // rf intentionally not set — RSC came after response-type variants
194
211
  return leaf;
195
212
  }
@@ -201,15 +218,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
201
218
  node.r = mergeLeaves(node.r, leaf);
202
219
  }
203
220
 
221
+ function buildLeaf(
222
+ leafBase: Omit<TrieLeaf, "pa">,
223
+ paramNames: string[],
224
+ ): TrieLeaf {
225
+ return paramNames.length > 0
226
+ ? { ...leafBase, pa: [...paramNames] }
227
+ : { ...leafBase };
228
+ }
229
+
204
230
  function insertSegments(
205
231
  node: TrieNode,
206
232
  segments: ParsedSegment[],
207
233
  index: number,
208
- leaf: TrieLeaf,
234
+ leafBase: Omit<TrieLeaf, "pa">,
235
+ paramNames: string[],
209
236
  ): void {
210
- // Base case: all segments consumed, add terminal
237
+ // Base case: all segments consumed, add terminal with branch-local pa
211
238
  if (index >= segments.length) {
212
- mergeLeaf(node, leaf);
239
+ mergeLeaf(node, buildLeaf(leafBase, paramNames));
213
240
  return;
214
241
  }
215
242
 
@@ -218,21 +245,47 @@ function insertSegments(
218
245
  if (segment.type === "static") {
219
246
  if (!node.s) node.s = {};
220
247
  if (!node.s[segment.value]) node.s[segment.value] = {};
221
- insertSegments(node.s[segment.value], segments, index + 1, leaf);
248
+ insertSegments(
249
+ node.s[segment.value],
250
+ segments,
251
+ index + 1,
252
+ leafBase,
253
+ paramNames,
254
+ );
222
255
  } else if (segment.type === "param") {
223
256
  if (segment.optional) {
224
- // Optional param: add terminal at current node (param absent)
225
- mergeLeaf(node, leaf);
226
- // AND continue with param child (param present)
257
+ // SKIP first: continue at the same node without binding this name.
258
+ // Skip-first ordering means the present-branch's TAKE overwrites any
259
+ // shared terminal later, giving greedy-leftmost semantics.
260
+ insertSegments(node, segments, index + 1, leafBase, paramNames);
227
261
  }
228
- if (!node.p) {
229
- node.p = { n: segment.value, c: {} };
262
+ if (segment.suffix) {
263
+ // Suffix param: keyed by suffix string (e.g., ".html")
264
+ if (!node.xp) node.xp = {};
265
+ if (!node.xp[segment.suffix]) {
266
+ node.xp[segment.suffix] = { n: segment.value, c: {} };
267
+ }
268
+ insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
269
+ ...paramNames,
270
+ segment.value,
271
+ ]);
272
+ } else {
273
+ if (!node.p) {
274
+ node.p = { n: segment.value, c: {} };
275
+ }
276
+ insertSegments(node.p.c, segments, index + 1, leafBase, [
277
+ ...paramNames,
278
+ segment.value,
279
+ ]);
230
280
  }
231
- insertSegments(node.p.c, segments, index + 1, leaf);
232
281
  } else if (segment.type === "wildcard") {
233
- // Wildcard consumes all remaining segments
234
- const wildLeaf = { ...leaf, pn: "*" };
235
- const existing = node.w ? { ...node.w } as TrieLeaf : undefined;
282
+ // Wildcard consumes all remaining segments. Carry any params bound before
283
+ // the wildcard in pa so they zip correctly against paramValues at match.
284
+ const wildLeaf: TrieLeaf & { pn: string } = {
285
+ ...buildLeaf(leafBase, paramNames),
286
+ pn: "*",
287
+ };
288
+ const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
236
289
  const merged = mergeLeaves(existing, wildLeaf);
237
290
  node.w = merged as TrieLeaf & { pn: string };
238
291
  }
@@ -0,0 +1,25 @@
1
+ import ts from "typescript";
2
+
3
+ export function getStringValue(node: ts.Node): string | null {
4
+ if (ts.isStringLiteral(node)) return node.text;
5
+ if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
6
+ return null;
7
+ }
8
+
9
+ export function extractObjectStringProperties(
10
+ node: ts.ObjectLiteralExpression,
11
+ ): Record<string, string> {
12
+ const result: Record<string, string> = {};
13
+ for (const prop of node.properties) {
14
+ if (!ts.isPropertyAssignment(prop)) continue;
15
+ const key = ts.isIdentifier(prop.name)
16
+ ? prop.name.text
17
+ : ts.isStringLiteral(prop.name)
18
+ ? prop.name.text
19
+ : null;
20
+ if (!key) continue;
21
+ const val = getStringValue(prop.initializer);
22
+ if (val !== null) result[key] = val;
23
+ }
24
+ return result;
25
+ }
@@ -0,0 +1,98 @@
1
+ import ts from "typescript";
2
+ import {
3
+ getStringValue,
4
+ extractObjectStringProperties,
5
+ } from "./ast-helpers.js";
6
+ import { extractParamsFromPattern } from "./param-extraction.js";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // AST-based route extraction
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * Extract route definitions from source code by walking the TypeScript AST.
14
+ * Finds path() and path.json(), path.md(), etc. call expressions and extracts
15
+ * the pattern, name, params, and optional search schema from each.
16
+ * Skips unnamed paths (no { name: "..." }).
17
+ */
18
+ export function extractRoutesFromSource(code: string): Array<{
19
+ name: string;
20
+ pattern: string;
21
+ params?: Record<string, string>;
22
+ search?: Record<string, string>;
23
+ }> {
24
+ const sourceFile = ts.createSourceFile(
25
+ "input.tsx",
26
+ code,
27
+ ts.ScriptTarget.Latest,
28
+ true,
29
+ ts.ScriptKind.TSX,
30
+ );
31
+ const routes: Array<{
32
+ name: string;
33
+ pattern: string;
34
+ params?: Record<string, string>;
35
+ search?: Record<string, string>;
36
+ }> = [];
37
+
38
+ function visit(node: ts.Node) {
39
+ if (ts.isCallExpression(node)) {
40
+ const callee = node.expression;
41
+ const isPath =
42
+ (ts.isIdentifier(callee) && callee.text === "path") ||
43
+ (ts.isPropertyAccessExpression(callee) &&
44
+ ts.isIdentifier(callee.expression) &&
45
+ callee.expression.text === "path");
46
+
47
+ if (isPath && node.arguments.length >= 1) {
48
+ const route = extractRouteFromCallExpression(node);
49
+ if (route) routes.push(route);
50
+ }
51
+ }
52
+ ts.forEachChild(node, visit);
53
+ }
54
+
55
+ visit(sourceFile);
56
+ return routes;
57
+ }
58
+
59
+ function extractRouteFromCallExpression(node: ts.CallExpression): {
60
+ name: string;
61
+ pattern: string;
62
+ params?: Record<string, string>;
63
+ search?: Record<string, string>;
64
+ } | null {
65
+ const patternNode = node.arguments[0];
66
+ const pattern = getStringValue(patternNode);
67
+ if (pattern === null) return null;
68
+
69
+ let name: string | null = null;
70
+ let search: Record<string, string> | undefined;
71
+
72
+ for (let i = 1; i < node.arguments.length; i++) {
73
+ const arg = node.arguments[i];
74
+ if (ts.isObjectLiteralExpression(arg)) {
75
+ for (const prop of arg.properties) {
76
+ if (!ts.isPropertyAssignment(prop)) continue;
77
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
78
+ if (propName === "name") {
79
+ name = getStringValue(prop.initializer);
80
+ } else if (
81
+ propName === "search" &&
82
+ ts.isObjectLiteralExpression(prop.initializer)
83
+ ) {
84
+ search = extractObjectStringProperties(prop.initializer);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (!name) return null;
91
+ const params = extractParamsFromPattern(pattern);
92
+ return {
93
+ name,
94
+ pattern,
95
+ ...(params ? { params } : {}),
96
+ ...(search && Object.keys(search).length > 0 ? { search } : {}),
97
+ };
98
+ }
@@ -0,0 +1,102 @@
1
+ import {
2
+ extractParamsFromPattern,
3
+ formatRouteEntry,
4
+ } from "./param-extraction.js";
5
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Code generation
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * Generate a per-module types file from extracted routes.
13
+ * Output has zero imports, preventing circular references.
14
+ */
15
+ export function generatePerModuleTypesSource(
16
+ routes: Array<{
17
+ name: string;
18
+ pattern: string;
19
+ params?: Record<string, string>;
20
+ search?: Record<string, string>;
21
+ }>,
22
+ ): string {
23
+ const valid = routes.filter(({ name }) => {
24
+ if (!name || /["'\\`\n\r]/.test(name)) {
25
+ console.warn(
26
+ `[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`,
27
+ );
28
+ return false;
29
+ }
30
+ return true;
31
+ });
32
+
33
+ // Deduplicate by name (first definition wins -- primary route before variants)
34
+ const deduped = new Map<
35
+ string,
36
+ {
37
+ pattern: string;
38
+ params?: Record<string, string>;
39
+ search?: Record<string, string>;
40
+ }
41
+ >();
42
+ for (const { name, pattern, params, search } of valid) {
43
+ if (deduped.has(name)) {
44
+ console.warn(
45
+ `[rsc-router] Duplicate route name "${name}" — keeping first definition`,
46
+ );
47
+ continue;
48
+ }
49
+ deduped.set(name, { pattern, params, search });
50
+ }
51
+ const sorted = [...deduped.entries()].sort(([a], [b]) => a.localeCompare(b));
52
+ const body = sorted
53
+ .map(([name, { pattern, params, search }]) => {
54
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
55
+ return formatRouteEntry(key, pattern, params, search);
56
+ })
57
+ .join("\n");
58
+ return `// Auto-generated by @rangojs/router - do not edit\nexport const routes = {\n${body}\n} as const;\nexport type routes = typeof routes;\n`;
59
+ }
60
+
61
+ /**
62
+ * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
63
+ * with route name -> pattern mappings. This enables Handler<"routeName">
64
+ * without circular references since the file has no imports from the app.
65
+ */
66
+ export function generateRouteTypesSource(
67
+ routeManifest: Record<string, string>,
68
+ searchSchemas?: Record<string, Record<string, string>>,
69
+ ): string {
70
+ const entries = Object.entries(routeManifest)
71
+ .filter(([name]) => !isAutoGeneratedRouteName(name))
72
+ .sort(([a], [b]) => a.localeCompare(b));
73
+
74
+ const filteredSearchSchemas = searchSchemas
75
+ ? Object.fromEntries(
76
+ Object.entries(searchSchemas).filter(
77
+ ([name]) => !isAutoGeneratedRouteName(name),
78
+ ),
79
+ )
80
+ : undefined;
81
+
82
+ const objectBody = entries
83
+ .map(([name, pattern]) => {
84
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
85
+ const params = extractParamsFromPattern(pattern);
86
+ const search = filteredSearchSchemas?.[name];
87
+ return formatRouteEntry(key, pattern, params, search);
88
+ })
89
+ .join("\n");
90
+
91
+ return `// Auto-generated by @rangojs/router - do not edit
92
+ export const NamedRoutes = {
93
+ ${objectBody}
94
+ } as const;
95
+
96
+ declare global {
97
+ namespace RSCRouter {
98
+ interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
99
+ }
100
+ }
101
+ `;
102
+ }