@rangojs/router 0.0.0-experimental.a769fbe7 → 0.0.0-experimental.ac99d918

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 (374) hide show
  1. package/README.md +211 -43
  2. package/dist/bin/rango.js +279 -102
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +3313 -1160
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +62 -11
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +63 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +222 -30
  12. package/skills/caching/SKILL.md +263 -8
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +250 -30
  18. package/skills/host-router/SKILL.md +124 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +223 -9
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +77 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +66 -5
  35. package/skills/router-setup/SKILL.md +38 -3
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +319 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/vercel/SKILL.md +107 -0
  57. package/skills/view-transitions/SKILL.md +294 -0
  58. package/src/__augment-tests__/augment.ts +81 -0
  59. package/src/__augment-tests__/augmented.check.ts +116 -0
  60. package/src/__internal.ts +1 -66
  61. package/src/browser/action-coordinator.ts +53 -36
  62. package/src/browser/action-fence.ts +47 -0
  63. package/src/browser/app-shell.ts +39 -0
  64. package/src/browser/app-version.ts +14 -0
  65. package/src/browser/cookie-name.ts +140 -0
  66. package/src/browser/event-controller.ts +81 -147
  67. package/src/browser/history-state.ts +21 -0
  68. package/src/browser/index.ts +3 -3
  69. package/src/browser/invalidate-client-cache.ts +52 -0
  70. package/src/browser/navigation-bridge.ts +66 -14
  71. package/src/browser/navigation-client.ts +172 -109
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +9 -59
  75. package/src/browser/partial-update.ts +79 -93
  76. package/src/browser/prefetch/cache.ts +180 -62
  77. package/src/browser/prefetch/fetch.ts +252 -39
  78. package/src/browser/prefetch/queue.ts +42 -8
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +72 -10
  81. package/src/browser/react/NavigationProvider.tsx +83 -31
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/context.ts +7 -2
  84. package/src/browser/react/filter-segment-order.ts +49 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +166 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +23 -69
  90. package/src/browser/react/use-link-status.ts +0 -4
  91. package/src/browser/react/use-navigation.ts +22 -5
  92. package/src/browser/react/use-params.ts +20 -10
  93. package/src/browser/react/use-reverse.ts +106 -0
  94. package/src/browser/react/use-router.ts +46 -11
  95. package/src/browser/react/use-search-params.ts +0 -5
  96. package/src/browser/react/use-segments.ts +11 -21
  97. package/src/browser/response-adapter.ts +52 -1
  98. package/src/browser/rsc-router.tsx +111 -24
  99. package/src/browser/scroll-restoration.ts +29 -19
  100. package/src/browser/segment-reconciler.ts +36 -14
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +176 -62
  103. package/src/browser/types.ts +60 -11
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +65 -40
  107. package/src/build/generate-route-types.ts +6 -0
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +137 -32
  111. package/src/build/route-types/codegen.ts +4 -4
  112. package/src/build/route-types/include-resolution.ts +9 -2
  113. package/src/build/route-types/param-extraction.ts +6 -3
  114. package/src/build/route-types/per-module-writer.ts +7 -4
  115. package/src/build/route-types/router-processing.ts +333 -94
  116. package/src/build/route-types/scan-filter.ts +9 -2
  117. package/src/build/route-types/source-scan.ts +118 -0
  118. package/src/build/runtime-discovery.ts +9 -20
  119. package/src/cache/cache-error.ts +104 -0
  120. package/src/cache/cache-policy.ts +68 -28
  121. package/src/cache/cache-runtime.ts +134 -32
  122. package/src/cache/cache-scope.ts +100 -74
  123. package/src/cache/cache-tag.ts +98 -0
  124. package/src/cache/cf/cf-cache-store.ts +2256 -241
  125. package/src/cache/cf/index.ts +6 -16
  126. package/src/cache/document-cache.ts +61 -20
  127. package/src/cache/handle-snapshot.ts +63 -0
  128. package/src/cache/index.ts +22 -20
  129. package/src/cache/memory-segment-store.ts +136 -37
  130. package/src/cache/profile-registry.ts +6 -30
  131. package/src/cache/read-through-swr.ts +41 -11
  132. package/src/cache/segment-codec.ts +0 -16
  133. package/src/cache/tag-invalidation.ts +230 -0
  134. package/src/cache/types.ts +33 -100
  135. package/src/cache/vercel/index.ts +11 -0
  136. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  137. package/src/client.rsc.tsx +6 -21
  138. package/src/client.tsx +108 -290
  139. package/src/component-utils.ts +19 -0
  140. package/src/context-var.ts +17 -5
  141. package/src/decode-loader-results.ts +36 -0
  142. package/src/defer.ts +196 -0
  143. package/src/deps/browser.ts +0 -1
  144. package/src/deps/ssr.ts +0 -1
  145. package/src/errors.ts +30 -4
  146. package/src/handle.ts +70 -22
  147. package/src/handles/MetaTags.tsx +0 -14
  148. package/src/handles/breadcrumbs.ts +16 -5
  149. package/src/handles/meta.ts +0 -39
  150. package/src/host/cookie-handler.ts +0 -36
  151. package/src/host/errors.ts +0 -24
  152. package/src/host/index.ts +8 -2
  153. package/src/host/pattern-matcher.ts +7 -50
  154. package/src/host/router.ts +107 -99
  155. package/src/host/testing.ts +40 -27
  156. package/src/host/types.ts +37 -4
  157. package/src/host/utils.ts +1 -1
  158. package/src/href-client.ts +137 -22
  159. package/src/index.rsc.ts +69 -10
  160. package/src/index.ts +112 -14
  161. package/src/internal-debug.ts +2 -4
  162. package/src/loader-store.ts +500 -0
  163. package/src/loader.rsc.ts +20 -13
  164. package/src/loader.ts +12 -11
  165. package/src/missing-id-error.ts +68 -0
  166. package/src/network-error-thrower.tsx +1 -6
  167. package/src/outlet-context.ts +1 -1
  168. package/src/outlet-provider.tsx +1 -5
  169. package/src/prerender/param-hash.ts +10 -11
  170. package/src/prerender/store.ts +37 -41
  171. package/src/prerender.ts +198 -82
  172. package/src/redirect-origin.ts +100 -0
  173. package/src/response-utils.ts +37 -0
  174. package/src/reverse.ts +65 -15
  175. package/src/root-error-boundary.tsx +1 -19
  176. package/src/route-content-wrapper.tsx +7 -72
  177. package/src/route-definition/dsl-helpers.ts +413 -275
  178. package/src/route-definition/helper-factories.ts +29 -139
  179. package/src/route-definition/helpers-types.ts +107 -32
  180. package/src/route-definition/index.ts +3 -0
  181. package/src/route-definition/redirect.ts +50 -8
  182. package/src/route-definition/resolve-handler-use.ts +161 -0
  183. package/src/route-definition/use-item-types.ts +32 -0
  184. package/src/route-map-builder.ts +0 -16
  185. package/src/route-types.ts +37 -41
  186. package/src/router/basename.ts +14 -0
  187. package/src/router/content-negotiation.ts +108 -9
  188. package/src/router/error-handling.ts +13 -17
  189. package/src/router/find-match.ts +44 -23
  190. package/src/router/handler-context.ts +46 -30
  191. package/src/router/intercept-resolution.ts +23 -23
  192. package/src/router/lazy-includes.ts +15 -52
  193. package/src/router/loader-resolution.ts +207 -30
  194. package/src/router/logging.ts +0 -6
  195. package/src/router/manifest.ts +40 -42
  196. package/src/router/match-api.ts +120 -204
  197. package/src/router/match-context.ts +0 -22
  198. package/src/router/match-handlers.ts +58 -58
  199. package/src/router/match-middleware/background-revalidation.ts +0 -7
  200. package/src/router/match-middleware/cache-lookup.ts +161 -262
  201. package/src/router/match-middleware/cache-store.ts +3 -33
  202. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  203. package/src/router/match-middleware/segment-resolution.ts +45 -14
  204. package/src/router/match-pipelines.ts +1 -42
  205. package/src/router/match-result.ts +87 -39
  206. package/src/router/metrics.ts +0 -34
  207. package/src/router/middleware-types.ts +7 -140
  208. package/src/router/middleware.ts +169 -140
  209. package/src/router/navigation-snapshot.ts +131 -0
  210. package/src/router/params-util.ts +23 -0
  211. package/src/router/pattern-matching.ts +109 -63
  212. package/src/router/prerender-match.ts +190 -54
  213. package/src/router/preview-match.ts +32 -102
  214. package/src/router/request-classification.ts +276 -0
  215. package/src/router/revalidation.ts +63 -55
  216. package/src/router/route-snapshot.ts +244 -0
  217. package/src/router/router-context.ts +0 -27
  218. package/src/router/router-interfaces.ts +100 -35
  219. package/src/router/router-options.ts +91 -11
  220. package/src/router/router-registry.ts +2 -5
  221. package/src/router/segment-resolution/fresh.ts +119 -65
  222. package/src/router/segment-resolution/helpers.ts +34 -0
  223. package/src/router/segment-resolution/loader-cache.ts +40 -37
  224. package/src/router/segment-resolution/revalidation.ts +329 -305
  225. package/src/router/segment-resolution/static-store.ts +19 -5
  226. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  227. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  228. package/src/router/segment-resolution.ts +4 -1
  229. package/src/router/segment-wrappers.ts +0 -3
  230. package/src/router/state-cookie-name.ts +33 -0
  231. package/src/router/substitute-pattern-params.ts +56 -0
  232. package/src/router/telemetry-otel.ts +0 -20
  233. package/src/router/telemetry.ts +96 -19
  234. package/src/router/timeout.ts +0 -20
  235. package/src/router/trie-matching.ts +91 -46
  236. package/src/router/types.ts +9 -63
  237. package/src/router/url-params.ts +44 -0
  238. package/src/router.ts +128 -42
  239. package/src/rsc/handler-context.ts +3 -2
  240. package/src/rsc/handler.ts +492 -409
  241. package/src/rsc/helpers.ts +162 -46
  242. package/src/rsc/index.ts +1 -1
  243. package/src/rsc/json-route-result.ts +38 -0
  244. package/src/rsc/loader-fetch.ts +18 -3
  245. package/src/rsc/manifest-init.ts +33 -42
  246. package/src/rsc/origin-guard.ts +39 -25
  247. package/src/rsc/progressive-enhancement.ts +28 -4
  248. package/src/rsc/redirect-guard.ts +99 -0
  249. package/src/rsc/response-error.ts +79 -12
  250. package/src/rsc/response-route-handler.ts +90 -63
  251. package/src/rsc/rsc-rendering.ts +53 -56
  252. package/src/rsc/runtime-warnings.ts +23 -10
  253. package/src/rsc/server-action.ts +74 -69
  254. package/src/rsc/ssr-setup.ts +18 -2
  255. package/src/rsc/types.ts +22 -9
  256. package/src/runtime-env.ts +18 -0
  257. package/src/search-params.ts +4 -20
  258. package/src/segment-content-promise.ts +67 -0
  259. package/src/segment-loader-promise.ts +134 -0
  260. package/src/segment-system.tsx +208 -201
  261. package/src/serialize.ts +243 -0
  262. package/src/server/context.ts +211 -52
  263. package/src/server/cookie-store.ts +80 -5
  264. package/src/server/handle-store.ts +26 -24
  265. package/src/server/loader-registry.ts +10 -28
  266. package/src/server/request-context.ts +289 -124
  267. package/src/ssr/index.tsx +22 -15
  268. package/src/static-handler.ts +27 -18
  269. package/src/testing/cache-status.ts +162 -0
  270. package/src/testing/collect-handle.ts +40 -0
  271. package/src/testing/dispatch.ts +618 -0
  272. package/src/testing/dom.entry.ts +22 -0
  273. package/src/testing/e2e/fixture.ts +188 -0
  274. package/src/testing/e2e/index.ts +128 -0
  275. package/src/testing/e2e/matchers.ts +35 -0
  276. package/src/testing/e2e/page-helpers.ts +272 -0
  277. package/src/testing/e2e/parity.ts +387 -0
  278. package/src/testing/e2e/server.ts +195 -0
  279. package/src/testing/flight-matchers.ts +97 -0
  280. package/src/testing/flight-normalize.ts +11 -0
  281. package/src/testing/flight-runtime.d.ts +57 -0
  282. package/src/testing/flight-tree.ts +682 -0
  283. package/src/testing/flight.entry.ts +52 -0
  284. package/src/testing/flight.ts +232 -0
  285. package/src/testing/generated-routes.ts +183 -0
  286. package/src/testing/index.ts +99 -0
  287. package/src/testing/internal/context.ts +348 -0
  288. package/src/testing/internal/flight-client-globals.ts +30 -0
  289. package/src/testing/internal/seed-vars.ts +54 -0
  290. package/src/testing/render-handler.ts +330 -0
  291. package/src/testing/render-route.tsx +566 -0
  292. package/src/testing/run-loader.ts +378 -0
  293. package/src/testing/run-middleware.ts +205 -0
  294. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  295. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  297. package/src/testing/vitest-stubs/version.ts +5 -0
  298. package/src/testing/vitest.ts +305 -0
  299. package/src/theme/ThemeProvider.tsx +0 -52
  300. package/src/theme/ThemeScript.tsx +0 -6
  301. package/src/theme/constants.ts +0 -12
  302. package/src/theme/index.ts +0 -7
  303. package/src/theme/theme-context.ts +1 -5
  304. package/src/theme/theme-script.ts +0 -14
  305. package/src/theme/use-theme.ts +0 -3
  306. package/src/types/boundaries.ts +0 -35
  307. package/src/types/cache-types.ts +17 -8
  308. package/src/types/error-types.ts +30 -90
  309. package/src/types/global-namespace.ts +54 -41
  310. package/src/types/handler-context.ts +124 -70
  311. package/src/types/index.ts +1 -10
  312. package/src/types/loader-types.ts +40 -11
  313. package/src/types/request-scope.ts +107 -0
  314. package/src/types/route-config.ts +6 -50
  315. package/src/types/route-entry.ts +12 -7
  316. package/src/types/segments.ts +36 -15
  317. package/src/urls/include-helper.ts +33 -70
  318. package/src/urls/index.ts +1 -11
  319. package/src/urls/path-helper-types.ts +58 -11
  320. package/src/urls/path-helper.ts +57 -111
  321. package/src/urls/pattern-types.ts +48 -19
  322. package/src/urls/response-types.ts +25 -22
  323. package/src/urls/type-extraction.ts +58 -139
  324. package/src/urls/urls-function.ts +1 -18
  325. package/src/use-loader.tsx +346 -89
  326. package/src/vite/debug.ts +185 -0
  327. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  328. package/src/vite/discovery/discover-routers.ts +130 -85
  329. package/src/vite/discovery/discovery-errors.ts +194 -0
  330. package/src/vite/discovery/gate-state.ts +171 -0
  331. package/src/vite/discovery/prerender-collection.ts +192 -99
  332. package/src/vite/discovery/route-types-writer.ts +40 -84
  333. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  334. package/src/vite/discovery/state.ts +51 -4
  335. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  336. package/src/vite/index.ts +8 -0
  337. package/src/vite/plugin-types.ts +236 -6
  338. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  339. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  340. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  341. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  342. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  343. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  344. package/src/vite/plugins/expose-action-id.ts +48 -95
  345. package/src/vite/plugins/expose-id-utils.ts +11 -50
  346. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  347. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  348. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  349. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  350. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  351. package/src/vite/plugins/performance-tracks.ts +65 -207
  352. package/src/vite/plugins/refresh-cmd.ts +89 -27
  353. package/src/vite/plugins/use-cache-transform.ts +73 -83
  354. package/src/vite/plugins/vercel-output.ts +258 -0
  355. package/src/vite/plugins/version-injector.ts +21 -25
  356. package/src/vite/plugins/version-plugin.ts +32 -23
  357. package/src/vite/plugins/virtual-entries.ts +46 -17
  358. package/src/vite/rango.ts +207 -125
  359. package/src/vite/router-discovery.ts +931 -133
  360. package/src/vite/utils/ast-handler-extract.ts +15 -31
  361. package/src/vite/utils/banner.ts +1 -1
  362. package/src/vite/utils/bundle-analysis.ts +10 -15
  363. package/src/vite/utils/client-chunks.ts +184 -0
  364. package/src/vite/utils/forward-user-plugins.ts +171 -0
  365. package/src/vite/utils/manifest-utils.ts +4 -59
  366. package/src/vite/utils/package-resolution.ts +20 -52
  367. package/src/vite/utils/prerender-utils.ts +27 -29
  368. package/src/vite/utils/shared-utils.ts +97 -44
  369. package/src/browser/action-response-classifier.ts +0 -99
  370. package/src/browser/debug-channel.ts +0 -93
  371. package/src/browser/react/use-client-cache.ts +0 -58
  372. package/src/browser/shallow.ts +0 -40
  373. package/src/handles/index.ts +0 -7
  374. package/src/router/middleware-cookies.ts +0 -55
@@ -15,6 +15,7 @@ import {
15
15
  import ts from "typescript";
16
16
  import { generateRouteTypesSource } from "./codegen.js";
17
17
  import type { ScanFilter } from "./scan-filter.js";
18
+ import { firstCodeMatchIndex } from "./source-scan.js";
18
19
  import {
19
20
  resolveImportedVariable,
20
21
  resolveImportPath,
@@ -38,6 +39,8 @@ function countPublicRouteEntries(source: string): number {
38
39
  }
39
40
 
40
41
  const ROUTER_CALL_PATTERN = /\bcreateRouter\s*[<(]/;
42
+ // Global variant for the code-region scan (firstCodeMatchIndex sets lastIndex).
43
+ const ROUTER_CALL_PATTERN_G = /\bcreateRouter\s*[<(]/g;
41
44
 
42
45
  function isRoutableSourceFile(name: string): boolean {
43
46
  return (
@@ -61,7 +64,7 @@ function findRouterFilesRecursive(
61
64
  entries = readdirSync(dir, { withFileTypes: true });
62
65
  } catch (err) {
63
66
  console.warn(
64
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
67
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
65
68
  );
66
69
  return;
67
70
  }
@@ -90,7 +93,17 @@ function findRouterFilesRecursive(
90
93
 
91
94
  try {
92
95
  const source = readFileSync(fullPath, "utf-8");
93
- if (ROUTER_CALL_PATTERN.test(source)) {
96
+ // Fast path: most files contain no `createRouter(` at all, so the cheap
97
+ // raw regex short-circuits before the code-region scan. Only a file that
98
+ // mentions the token (real call OR a comment/string mention) is rescanned
99
+ // over code regions — allocation-free, never building a stripped copy —
100
+ // so a mention inside a comment or string is not mistaken for a real
101
+ // router file (which previously triggered a spurious "Multiple routers
102
+ // found" error).
103
+ if (
104
+ ROUTER_CALL_PATTERN.test(source) &&
105
+ firstCodeMatchIndex(source, ROUTER_CALL_PATTERN_G) >= 0
106
+ ) {
94
107
  routerFilesInDir.push(fullPath);
95
108
  }
96
109
  } catch {
@@ -142,7 +155,7 @@ export function findNestedRouterConflict(
142
155
 
143
156
  export function formatNestedRouterConflictError(
144
157
  conflict: { ancestor: string; nested: string },
145
- prefix = "[rsc-router]",
158
+ prefix = "[rango]",
146
159
  ): string {
147
160
  return (
148
161
  `${prefix} Nested router roots are not supported.\n` +
@@ -157,13 +170,26 @@ export function formatNestedRouterConflictError(
157
170
  // ---------------------------------------------------------------------------
158
171
 
159
172
  /**
160
- * Extract the url patterns variable from a router file using AST.
161
- * Detects two patterns:
173
+ * Result of extracting URL patterns from a router file.
174
+ * - "variable": a named variable reference (e.g., `.routes(patterns)` or `urls: patterns`)
175
+ * - "inline": an inline builder function (e.g., `.routes(({ path }) => [...])` or `urls: ({ path }) => [...]`)
176
+ */
177
+ export type UrlsExtractionResult =
178
+ | { kind: "variable"; name: string }
179
+ | { kind: "inline"; block: string };
180
+
181
+ /**
182
+ * Extract the url patterns from a router file using AST.
183
+ * Detects four patterns:
162
184
  * 1. createRouter(...).routes(variableName)
163
185
  * 2. createRouter({ urls: variableName, ... })
164
- * Returns the local variable name.
186
+ * 3. createRouter(...).routes(({ path, ... }) => [...])
187
+ * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
188
+ * Returns either a variable name or an inline code block.
165
189
  */
166
- export function extractUrlsVariableFromRouter(code: string): string | null {
190
+ export function extractUrlsFromRouter(
191
+ code: string,
192
+ ): UrlsExtractionResult | null {
167
193
  const sourceFile = ts.createSourceFile(
168
194
  "router.tsx",
169
195
  code,
@@ -171,7 +197,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
171
197
  true,
172
198
  ts.ScriptKind.TSX,
173
199
  );
174
- let result: string | null = null;
200
+ let result: UrlsExtractionResult | null = null;
175
201
 
176
202
  function isCreateRouterCall(node: ts.Node): boolean {
177
203
  if (!ts.isCallExpression(node)) return false;
@@ -179,44 +205,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
179
205
  return ts.isIdentifier(callee) && callee.text === "createRouter";
180
206
  }
181
207
 
208
+ /** Check if a node is an arrow/function expression (inline builder). */
209
+ function isInlineBuilder(node: ts.Node): boolean {
210
+ return ts.isArrowFunction(node) || ts.isFunctionExpression(node);
211
+ }
212
+
213
+ /** Check if a .routes() call chains from createRouter(). */
214
+ function isRoutesOnCreateRouter(node: ts.CallExpression): boolean {
215
+ if (
216
+ !ts.isPropertyAccessExpression(node.expression) ||
217
+ node.expression.name.text !== "routes"
218
+ )
219
+ return false;
220
+ let inner: ts.Expression = node.expression.expression;
221
+ while (
222
+ ts.isCallExpression(inner) &&
223
+ ts.isPropertyAccessExpression(inner.expression)
224
+ ) {
225
+ inner = inner.expression.expression;
226
+ }
227
+ return isCreateRouterCall(inner);
228
+ }
229
+
182
230
  function visit(node: ts.Node) {
183
231
  if (result) return;
184
232
 
185
- // Pattern 1: createRouter(...).routes(variableName)
186
- // The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
233
+ // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
187
234
  if (
188
235
  ts.isCallExpression(node) &&
189
- ts.isPropertyAccessExpression(node.expression) &&
190
- node.expression.name.text === "routes" &&
191
236
  node.arguments.length >= 1 &&
192
- ts.isIdentifier(node.arguments[0])
237
+ isRoutesOnCreateRouter(node)
193
238
  ) {
194
- // Walk up the chain: createRouter().middleware(...).routes(x) etc.
195
- // The innermost call should be createRouter(...)
196
- let inner: ts.Expression = node.expression.expression;
197
- while (
198
- ts.isCallExpression(inner) &&
199
- ts.isPropertyAccessExpression(inner.expression)
200
- ) {
201
- inner = inner.expression.expression;
202
- }
203
- if (isCreateRouterCall(inner)) {
204
- result = (node.arguments[0] as ts.Identifier).text;
205
- return;
239
+ const arg = node.arguments[0];
240
+ if (ts.isIdentifier(arg)) {
241
+ result = { kind: "variable", name: arg.text };
242
+ } else if (isInlineBuilder(arg)) {
243
+ result = { kind: "inline", block: arg.getText(sourceFile) };
206
244
  }
245
+ return;
207
246
  }
208
247
 
209
- // Pattern 2: createRouter({ urls: variableName, ... })
248
+ // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
210
249
  if (isCreateRouterCall(node)) {
211
250
  const callExpr = node as ts.CallExpression;
212
- for (const arg of callExpr.arguments) {
251
+ for (const callArg of callExpr.arguments) {
252
+ if (ts.isObjectLiteralExpression(callArg)) {
253
+ for (const prop of callArg.properties) {
254
+ if (
255
+ ts.isPropertyAssignment(prop) &&
256
+ ts.isIdentifier(prop.name) &&
257
+ prop.name.text === "urls"
258
+ ) {
259
+ if (ts.isIdentifier(prop.initializer)) {
260
+ result = { kind: "variable", name: prop.initializer.text };
261
+ } else if (isInlineBuilder(prop.initializer)) {
262
+ result = {
263
+ kind: "inline",
264
+ block: prop.initializer.getText(sourceFile),
265
+ };
266
+ }
267
+ return;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+
274
+ ts.forEachChild(node, visit);
275
+ }
276
+
277
+ visit(sourceFile);
278
+ return result;
279
+ }
280
+
281
+ /**
282
+ * Extract the `basename` string literal from createRouter({ basename: "..." }).
283
+ * Returns the basename value or undefined if not present.
284
+ */
285
+ export function extractBasenameFromRouter(code: string): string | undefined {
286
+ const sourceFile = ts.createSourceFile(
287
+ "router.tsx",
288
+ code,
289
+ ts.ScriptTarget.Latest,
290
+ true,
291
+ ts.ScriptKind.TSX,
292
+ );
293
+ let result: string | undefined;
294
+
295
+ function visit(node: ts.Node) {
296
+ if (result !== undefined) return;
297
+ if (
298
+ ts.isCallExpression(node) &&
299
+ ts.isIdentifier(node.expression) &&
300
+ node.expression.text === "createRouter"
301
+ ) {
302
+ for (const arg of node.arguments) {
213
303
  if (ts.isObjectLiteralExpression(arg)) {
214
304
  for (const prop of arg.properties) {
215
305
  if (
216
306
  ts.isPropertyAssignment(prop) &&
217
307
  ts.isIdentifier(prop.name) &&
218
- prop.name.text === "urls" &&
219
- ts.isIdentifier(prop.initializer)
308
+ prop.name.text === "basename" &&
309
+ ts.isStringLiteral(prop.initializer)
220
310
  ) {
221
311
  result = prop.initializer.text;
222
312
  return;
@@ -225,7 +315,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
225
315
  }
226
316
  }
227
317
  }
228
-
229
318
  ts.forEachChild(node, visit);
230
319
  }
231
320
 
@@ -233,9 +322,70 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
233
322
  return result;
234
323
  }
235
324
 
325
+ /** @deprecated Use extractUrlsFromRouter instead */
326
+ export function extractUrlsVariableFromRouter(code: string): string | null {
327
+ const result = extractUrlsFromRouter(code);
328
+ return result?.kind === "variable" ? result.name : null;
329
+ }
330
+
331
+ /** Apply a basename prefix to all route patterns in a result set. */
332
+ function applyBasenameToRoutes(
333
+ result: {
334
+ routes: Record<string, string>;
335
+ searchSchemas: Record<string, Record<string, string>>;
336
+ },
337
+ basename: string,
338
+ ): {
339
+ routes: Record<string, string>;
340
+ searchSchemas: Record<string, Record<string, string>>;
341
+ } {
342
+ const prefixed: Record<string, string> = {};
343
+ for (const [name, pattern] of Object.entries(result.routes)) {
344
+ if (pattern === "/") {
345
+ prefixed[name] = basename;
346
+ } else if (basename.endsWith("/") && pattern.startsWith("/")) {
347
+ prefixed[name] = basename + pattern.slice(1);
348
+ } else {
349
+ prefixed[name] = basename + pattern;
350
+ }
351
+ }
352
+ return { routes: prefixed, searchSchemas: result.searchSchemas };
353
+ }
354
+
355
+ // Filesystem path of the generated route-types file for a router source file.
356
+ // Native separators — matches the self-gen-tracking Map key the watcher compares.
357
+ export function genFileTsPath(sourceFile: string): string {
358
+ const base = pathBasename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
359
+ return join(dirname(sourceFile), `${base}.named-routes.gen.ts`);
360
+ }
361
+
362
+ // Search schemas for the gen file: prefer the runtime manifest's; when it omits
363
+ // them (some module-runner flows) fall back to static parsing filtered to the
364
+ // public route-name set. Returns the runtime value unchanged otherwise.
365
+ export function resolveSearchSchemas(
366
+ publicRouteNames: string[],
367
+ runtimeSchemas: Record<string, Record<string, string>> | undefined,
368
+ sourceFile: string,
369
+ ): Record<string, Record<string, string>> | undefined {
370
+ if (runtimeSchemas && Object.keys(runtimeSchemas).length > 0) {
371
+ return runtimeSchemas;
372
+ }
373
+ const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
374
+ if (Object.keys(staticParsed.searchSchemas).length === 0) {
375
+ return runtimeSchemas;
376
+ }
377
+ const filtered: Record<string, Record<string, string>> = {};
378
+ for (const name of publicRouteNames) {
379
+ const schema = staticParsed.searchSchemas[name];
380
+ if (schema) filtered[name] = schema;
381
+ }
382
+ return Object.keys(filtered).length > 0 ? filtered : runtimeSchemas;
383
+ }
384
+
236
385
  /**
237
386
  * Resolve routes and search schemas from a router source file by following the
238
- * variable passed to `.routes(...)` or `urls: ...` in createRouter options.
387
+ * variable passed to `.routes(...)` or `urls: ...` in createRouter options,
388
+ * or by parsing an inline builder function directly.
239
389
  */
240
390
  export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
241
391
  routes: Record<string, string>;
@@ -248,21 +398,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
248
398
  return { routes: {}, searchSchemas: {} };
249
399
  }
250
400
 
251
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
252
- if (!urlsVarName) {
401
+ const extraction = extractUrlsFromRouter(routerSource);
402
+ if (!extraction) {
253
403
  return { routes: {}, searchSchemas: {} };
254
404
  }
255
405
 
256
- const imported = resolveImportedVariable(routerSource, urlsVarName);
257
- if (imported) {
258
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
259
- if (!targetFile) {
260
- return { routes: {}, searchSchemas: {} };
406
+ // Detect basename from createRouter({ basename: "..." })
407
+ const rawBasename = extractBasenameFromRouter(routerSource);
408
+ const basename = rawBasename
409
+ ? ("/" + rawBasename.replace(/^\/+|\/+$/g, "")).replace(/^\/$/, "")
410
+ : undefined;
411
+
412
+ let result: {
413
+ routes: Record<string, string>;
414
+ searchSchemas: Record<string, Record<string, string>>;
415
+ };
416
+
417
+ // Inline builder: extract routes directly from the function body
418
+ if (extraction.kind === "inline") {
419
+ result = buildCombinedRouteMapWithSearch(
420
+ routerFilePath,
421
+ undefined,
422
+ undefined,
423
+ undefined,
424
+ extraction.block,
425
+ );
426
+ } else {
427
+ // Variable reference: follow imports or same-file declaration
428
+ const imported = resolveImportedVariable(routerSource, extraction.name);
429
+ if (imported) {
430
+ const targetFile = resolveImportPath(imported.specifier, routerFilePath);
431
+ if (!targetFile) {
432
+ return { routes: {}, searchSchemas: {} };
433
+ }
434
+ result = buildCombinedRouteMapWithSearch(
435
+ targetFile,
436
+ imported.exportedName,
437
+ );
438
+ } else {
439
+ result = buildCombinedRouteMapWithSearch(routerFilePath, extraction.name);
261
440
  }
262
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
263
441
  }
264
442
 
265
- return buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
443
+ // Apply basename prefix to all extracted route patterns
444
+ if (basename) {
445
+ result = applyBasenameToRoutes(result, basename);
446
+ }
447
+
448
+ return result;
266
449
  }
267
450
 
268
451
  // ---------------------------------------------------------------------------
@@ -285,12 +468,26 @@ export function detectUnresolvableIncludes(
285
468
  return [];
286
469
  }
287
470
 
288
- // Extract the urls variable from the router file
289
- const urlsVarName = extractUrlsVariableFromRouter(source);
290
- if (!urlsVarName) return [];
471
+ // Extract the urls source from the router file
472
+ const extraction = extractUrlsFromRouter(source);
473
+ if (!extraction) return [];
474
+
475
+ const diagnostics: UnresolvableInclude[] = [];
476
+
477
+ if (extraction.kind === "inline") {
478
+ // Inline builder: parse directly
479
+ buildCombinedRouteMapWithSearch(
480
+ realPath,
481
+ undefined,
482
+ new Set(),
483
+ diagnostics,
484
+ extraction.block,
485
+ );
486
+ return diagnostics;
487
+ }
291
488
 
292
- // Resolve where the urls variable comes from
293
- const imported = resolveImportedVariable(source, urlsVarName);
489
+ // Variable reference: resolve where it comes from
490
+ const imported = resolveImportedVariable(source, extraction.name);
294
491
  let targetFile: string;
295
492
  let exportedName: string | undefined;
296
493
 
@@ -312,10 +509,9 @@ export function detectUnresolvableIncludes(
312
509
  } else {
313
510
  // Same-file urls() definition
314
511
  targetFile = realPath;
315
- exportedName = urlsVarName;
512
+ exportedName = extraction.name;
316
513
  }
317
514
 
318
- const diagnostics: UnresolvableInclude[] = [];
319
515
  buildCombinedRouteMapWithSearch(
320
516
  targetFile,
321
517
  exportedName,
@@ -366,6 +562,73 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
366
562
  return result;
367
563
  }
368
564
 
565
+ const HOST_ROUTER_CALL_PATTERN = /\bcreateHostRouter\s*[<(]/;
566
+ const HOST_ROUTER_CALL_PATTERN_G = /\bcreateHostRouter\s*[<(]/g;
567
+
568
+ function findHostRouterFilesRecursive(
569
+ dir: string,
570
+ filter: ScanFilter | undefined,
571
+ results: string[],
572
+ ): void {
573
+ let entries;
574
+ try {
575
+ entries = readdirSync(dir, { withFileTypes: true });
576
+ } catch (err) {
577
+ console.warn(
578
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
579
+ );
580
+ return;
581
+ }
582
+
583
+ for (const entry of entries) {
584
+ const fullPath = join(dir, entry.name);
585
+ if (entry.isDirectory()) {
586
+ if (
587
+ entry.name === "node_modules" ||
588
+ entry.name === "dist" ||
589
+ entry.name === "coverage" ||
590
+ entry.name === "__tests__" ||
591
+ entry.name === "__mocks__" ||
592
+ entry.name.startsWith(".")
593
+ )
594
+ continue;
595
+ // Unlike createRouter scanning, do NOT stop at router roots: the host entry
596
+ // lives above the sub-app router directories, so descend the whole tree.
597
+ findHostRouterFilesRecursive(fullPath, filter, results);
598
+ continue;
599
+ }
600
+
601
+ if (!isRoutableSourceFile(entry.name)) continue;
602
+ if (filter && !filter(fullPath)) continue;
603
+
604
+ try {
605
+ const source = readFileSync(fullPath, "utf-8");
606
+ if (
607
+ HOST_ROUTER_CALL_PATTERN.test(source) &&
608
+ firstCodeMatchIndex(source, HOST_ROUTER_CALL_PATTERN_G) >= 0
609
+ ) {
610
+ results.push(fullPath);
611
+ }
612
+ } catch {
613
+ continue;
614
+ }
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Scan for files containing createHostRouter() and return their paths. Unlike
620
+ * findRouterFiles, this does NOT stop at the first router-root directory -- a host
621
+ * entry typically sits above the sub-app router roots, so the whole tree is scanned.
622
+ */
623
+ export function findHostRouterFiles(
624
+ root: string,
625
+ filter?: ScanFilter,
626
+ ): string[] {
627
+ const result: string[] = [];
628
+ findHostRouterFilesRecursive(root, filter, result);
629
+ return result;
630
+ }
631
+
369
632
  /**
370
633
  * Write named-routes.gen.ts files from static source parsing.
371
634
  * Dev-only: provides initial .gen.ts files for IDE types before runtime
@@ -375,7 +638,10 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
375
638
  export function writeCombinedRouteTypes(
376
639
  root: string,
377
640
  knownRouterFiles?: string[],
378
- opts?: { preserveIfLarger?: boolean },
641
+ opts?: {
642
+ preserveIfLarger?: boolean;
643
+ onWrite?: (outPath: string, content: string) => void;
644
+ },
379
645
  ): void {
380
646
  // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
381
647
  try {
@@ -383,7 +649,7 @@ export function writeCombinedRouteTypes(
383
649
  if (existsSync(oldCombinedPath)) {
384
650
  unlinkSync(oldCombinedPath);
385
651
  console.log(
386
- `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
652
+ `[rango] Removed stale combined route types: ${oldCombinedPath}`,
387
653
  );
388
654
  }
389
655
  } catch {}
@@ -397,54 +663,31 @@ export function writeCombinedRouteTypes(
397
663
  }
398
664
 
399
665
  for (const routerFilePath of routerFilePaths) {
400
- let routerSource: string;
401
- try {
402
- routerSource = readFileSync(routerFilePath, "utf-8");
403
- } catch {
404
- continue;
405
- }
406
- // Extract the urls variable name from .routes(varName) or urls: varName
407
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
408
- if (!urlsVarName) continue;
409
-
410
- // Resolve the variable to its source module
411
- let result: {
412
- routes: Record<string, string>;
413
- searchSchemas: Record<string, Record<string, string>>;
414
- };
415
-
416
- const imported = resolveImportedVariable(routerSource, urlsVarName);
417
- if (imported) {
418
- // Variable is imported from another module
419
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
420
- if (!targetFile) continue;
421
- result = buildCombinedRouteMapWithSearch(
422
- targetFile,
423
- imported.exportedName,
424
- );
425
- } else {
426
- // Variable is defined in the same file
427
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
666
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
667
+ if (
668
+ Object.keys(result.routes).length === 0 &&
669
+ Object.keys(result.searchSchemas).length === 0
670
+ ) {
671
+ // Check if the file even has a createRouter call — if not, skip entirely.
672
+ // If it does, fall through to write an empty placeholder below.
673
+ let routerSource: string;
674
+ try {
675
+ routerSource = readFileSync(routerFilePath, "utf-8");
676
+ } catch {
677
+ continue;
678
+ }
679
+ if (!extractUrlsFromRouter(routerSource)) continue;
428
680
  }
429
681
 
430
- const routerBasename = pathBasename(routerFilePath).replace(
431
- /\.(tsx?|jsx?)$/,
432
- "",
433
- );
434
- const outPath = join(
435
- dirname(routerFilePath),
436
- `${routerBasename}.named-routes.gen.ts`,
437
- );
682
+ const outPath = genFileTsPath(routerFilePath);
438
683
  const existing = existsSync(outPath)
439
684
  ? readFileSync(outPath, "utf-8")
440
685
  : null;
441
686
 
442
- // When the static parser can't extract routes (e.g. callback-style urls()),
443
- // write an empty placeholder so the build-time transform's injected import
444
- // resolves. Runtime discovery will overwrite this with the real routes.
445
687
  if (Object.keys(result.routes).length === 0) {
446
688
  if (!existing) {
447
689
  const emptySource = generateRouteTypesSource({});
690
+ opts?.onWrite?.(outPath, emptySource);
448
691
  writeFileSync(outPath, emptySource);
449
692
  }
450
693
  continue;
@@ -456,11 +699,6 @@ export function writeCombinedRouteTypes(
456
699
  hasSearchSchemas ? result.searchSchemas : undefined,
457
700
  );
458
701
  if (existing !== source) {
459
- // On initial dev startup, don't overwrite a file from runtime discovery
460
- // (which has all dynamic routes) with a smaller set from the static
461
- // parser. The static parser can't see routes generated by Array.from()
462
- // or other dynamic code. During HMR (file watcher), always write so
463
- // newly added routes appear immediately.
464
702
  if (opts?.preserveIfLarger && existing) {
465
703
  const existingCount = countPublicRouteEntries(existing);
466
704
  const newCount = Object.keys(result.routes).filter(
@@ -470,9 +708,10 @@ export function writeCombinedRouteTypes(
470
708
  continue;
471
709
  }
472
710
  }
711
+ opts?.onWrite?.(outPath, source);
473
712
  writeFileSync(outPath, source);
474
713
  console.log(
475
- `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
714
+ `[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
476
715
  );
477
716
  }
478
717
  }
@@ -54,14 +54,21 @@ export function findTsFiles(dir: string, filter?: ScanFilter): string[] {
54
54
  entries = readdirSync(dir, { withFileTypes: true });
55
55
  } catch (err) {
56
56
  console.warn(
57
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
57
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
58
58
  );
59
59
  return results;
60
60
  }
61
61
  for (const entry of entries) {
62
62
  const fullPath = join(dir, entry.name);
63
63
  if (entry.isDirectory()) {
64
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
64
+ if (
65
+ entry.name === "node_modules" ||
66
+ entry.name.startsWith(".") ||
67
+ entry.name === "dist" ||
68
+ entry.name === "build" ||
69
+ entry.name === "coverage"
70
+ )
71
+ continue;
65
72
  results.push(...findTsFiles(fullPath, filter));
66
73
  } else if (
67
74
  (entry.name.endsWith(".ts") ||