@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  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 +270 -30
  18. package/skills/host-router/SKILL.md +82 -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 +294 -30
  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 +203 -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 +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  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 +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  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 +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. 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 (
@@ -45,7 +48,9 @@ function isRoutableSourceFile(name: string): boolean {
45
48
  name.endsWith(".tsx") ||
46
49
  name.endsWith(".js") ||
47
50
  name.endsWith(".jsx")) &&
48
- !name.includes(".gen.")
51
+ !name.includes(".gen.") &&
52
+ !name.includes(".test.") &&
53
+ !name.includes(".spec.")
49
54
  );
50
55
  }
51
56
 
@@ -59,7 +64,7 @@ function findRouterFilesRecursive(
59
64
  entries = readdirSync(dir, { withFileTypes: true });
60
65
  } catch (err) {
61
66
  console.warn(
62
- `[rsc-router] Failed to scan directory ${dir}: ${(err as Error).message}`,
67
+ `[rango] Failed to scan directory ${dir}: ${(err as Error).message}`,
63
68
  );
64
69
  return;
65
70
  }
@@ -70,7 +75,15 @@ function findRouterFilesRecursive(
70
75
  for (const entry of entries) {
71
76
  const fullPath = join(dir, entry.name);
72
77
  if (entry.isDirectory()) {
73
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
78
+ if (
79
+ entry.name === "node_modules" ||
80
+ entry.name === "dist" ||
81
+ entry.name === "coverage" ||
82
+ entry.name === "__tests__" ||
83
+ entry.name === "__mocks__" ||
84
+ entry.name.startsWith(".")
85
+ )
86
+ continue;
74
87
  childDirs.push(fullPath);
75
88
  continue;
76
89
  }
@@ -80,7 +93,17 @@ function findRouterFilesRecursive(
80
93
 
81
94
  try {
82
95
  const source = readFileSync(fullPath, "utf-8");
83
- 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
+ ) {
84
107
  routerFilesInDir.push(fullPath);
85
108
  }
86
109
  } catch {
@@ -132,7 +155,7 @@ export function findNestedRouterConflict(
132
155
 
133
156
  export function formatNestedRouterConflictError(
134
157
  conflict: { ancestor: string; nested: string },
135
- prefix = "[rsc-router]",
158
+ prefix = "[rango]",
136
159
  ): string {
137
160
  return (
138
161
  `${prefix} Nested router roots are not supported.\n` +
@@ -147,13 +170,26 @@ export function formatNestedRouterConflictError(
147
170
  // ---------------------------------------------------------------------------
148
171
 
149
172
  /**
150
- * Extract the url patterns variable from a router file using AST.
151
- * 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:
152
184
  * 1. createRouter(...).routes(variableName)
153
185
  * 2. createRouter({ urls: variableName, ... })
154
- * 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.
155
189
  */
156
- export function extractUrlsVariableFromRouter(code: string): string | null {
190
+ export function extractUrlsFromRouter(
191
+ code: string,
192
+ ): UrlsExtractionResult | null {
157
193
  const sourceFile = ts.createSourceFile(
158
194
  "router.tsx",
159
195
  code,
@@ -161,7 +197,7 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
161
197
  true,
162
198
  ts.ScriptKind.TSX,
163
199
  );
164
- let result: string | null = null;
200
+ let result: UrlsExtractionResult | null = null;
165
201
 
166
202
  function isCreateRouterCall(node: ts.Node): boolean {
167
203
  if (!ts.isCallExpression(node)) return false;
@@ -169,44 +205,108 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
169
205
  return ts.isIdentifier(callee) && callee.text === "createRouter";
170
206
  }
171
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
+
172
230
  function visit(node: ts.Node) {
173
231
  if (result) return;
174
232
 
175
- // Pattern 1: createRouter(...).routes(variableName)
176
- // The AST shape is CallExpression(.routes) -> PropertyAccessExpression -> CallExpression(createRouter)
233
+ // Pattern 1 & 3: createRouter(...).routes(variableName | builder)
177
234
  if (
178
235
  ts.isCallExpression(node) &&
179
- ts.isPropertyAccessExpression(node.expression) &&
180
- node.expression.name.text === "routes" &&
181
236
  node.arguments.length >= 1 &&
182
- ts.isIdentifier(node.arguments[0])
237
+ isRoutesOnCreateRouter(node)
183
238
  ) {
184
- // Walk up the chain: createRouter().middleware(...).routes(x) etc.
185
- // The innermost call should be createRouter(...)
186
- let inner: ts.Expression = node.expression.expression;
187
- while (
188
- ts.isCallExpression(inner) &&
189
- ts.isPropertyAccessExpression(inner.expression)
190
- ) {
191
- inner = inner.expression.expression;
192
- }
193
- if (isCreateRouterCall(inner)) {
194
- result = (node.arguments[0] as ts.Identifier).text;
195
- 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) };
196
244
  }
245
+ return;
197
246
  }
198
247
 
199
- // Pattern 2: createRouter({ urls: variableName, ... })
248
+ // Pattern 2 & 4: createRouter({ urls: variableName | builder, ... })
200
249
  if (isCreateRouterCall(node)) {
201
250
  const callExpr = node as ts.CallExpression;
202
- 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) {
203
303
  if (ts.isObjectLiteralExpression(arg)) {
204
304
  for (const prop of arg.properties) {
205
305
  if (
206
306
  ts.isPropertyAssignment(prop) &&
207
307
  ts.isIdentifier(prop.name) &&
208
- prop.name.text === "urls" &&
209
- ts.isIdentifier(prop.initializer)
308
+ prop.name.text === "basename" &&
309
+ ts.isStringLiteral(prop.initializer)
210
310
  ) {
211
311
  result = prop.initializer.text;
212
312
  return;
@@ -215,7 +315,6 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
215
315
  }
216
316
  }
217
317
  }
218
-
219
318
  ts.forEachChild(node, visit);
220
319
  }
221
320
 
@@ -223,9 +322,70 @@ export function extractUrlsVariableFromRouter(code: string): string | null {
223
322
  return result;
224
323
  }
225
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
+
226
385
  /**
227
386
  * Resolve routes and search schemas from a router source file by following the
228
- * 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.
229
389
  */
230
390
  export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
231
391
  routes: Record<string, string>;
@@ -238,21 +398,54 @@ export function buildCombinedRouteMapForRouterFile(routerFilePath: string): {
238
398
  return { routes: {}, searchSchemas: {} };
239
399
  }
240
400
 
241
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
242
- if (!urlsVarName) {
401
+ const extraction = extractUrlsFromRouter(routerSource);
402
+ if (!extraction) {
243
403
  return { routes: {}, searchSchemas: {} };
244
404
  }
245
405
 
246
- const imported = resolveImportedVariable(routerSource, urlsVarName);
247
- if (imported) {
248
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
249
- if (!targetFile) {
250
- 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);
251
440
  }
252
- return buildCombinedRouteMapWithSearch(targetFile, imported.exportedName);
253
441
  }
254
442
 
255
- 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;
256
449
  }
257
450
 
258
451
  // ---------------------------------------------------------------------------
@@ -275,12 +468,26 @@ export function detectUnresolvableIncludes(
275
468
  return [];
276
469
  }
277
470
 
278
- // Extract the urls variable from the router file
279
- const urlsVarName = extractUrlsVariableFromRouter(source);
280
- 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[] = [];
281
476
 
282
- // Resolve where the urls variable comes from
283
- const imported = resolveImportedVariable(source, urlsVarName);
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
+ }
488
+
489
+ // Variable reference: resolve where it comes from
490
+ const imported = resolveImportedVariable(source, extraction.name);
284
491
  let targetFile: string;
285
492
  let exportedName: string | undefined;
286
493
 
@@ -302,10 +509,9 @@ export function detectUnresolvableIncludes(
302
509
  } else {
303
510
  // Same-file urls() definition
304
511
  targetFile = realPath;
305
- exportedName = urlsVarName;
512
+ exportedName = extraction.name;
306
513
  }
307
514
 
308
- const diagnostics: UnresolvableInclude[] = [];
309
515
  buildCombinedRouteMapWithSearch(
310
516
  targetFile,
311
517
  exportedName,
@@ -365,7 +571,10 @@ export function findRouterFiles(root: string, filter?: ScanFilter): string[] {
365
571
  export function writeCombinedRouteTypes(
366
572
  root: string,
367
573
  knownRouterFiles?: string[],
368
- opts?: { preserveIfLarger?: boolean },
574
+ opts?: {
575
+ preserveIfLarger?: boolean;
576
+ onWrite?: (outPath: string, content: string) => void;
577
+ },
369
578
  ): void {
370
579
  // Delete old combined named-routes.gen.ts if it exists (stale from older versions)
371
580
  try {
@@ -373,7 +582,7 @@ export function writeCombinedRouteTypes(
373
582
  if (existsSync(oldCombinedPath)) {
374
583
  unlinkSync(oldCombinedPath);
375
584
  console.log(
376
- `[rsc-router] Removed stale combined route types: ${oldCombinedPath}`,
585
+ `[rango] Removed stale combined route types: ${oldCombinedPath}`,
377
586
  );
378
587
  }
379
588
  } catch {}
@@ -387,54 +596,31 @@ export function writeCombinedRouteTypes(
387
596
  }
388
597
 
389
598
  for (const routerFilePath of routerFilePaths) {
390
- let routerSource: string;
391
- try {
392
- routerSource = readFileSync(routerFilePath, "utf-8");
393
- } catch {
394
- continue;
395
- }
396
- // Extract the urls variable name from .routes(varName) or urls: varName
397
- const urlsVarName = extractUrlsVariableFromRouter(routerSource);
398
- if (!urlsVarName) continue;
399
-
400
- // Resolve the variable to its source module
401
- let result: {
402
- routes: Record<string, string>;
403
- searchSchemas: Record<string, Record<string, string>>;
404
- };
405
-
406
- const imported = resolveImportedVariable(routerSource, urlsVarName);
407
- if (imported) {
408
- // Variable is imported from another module
409
- const targetFile = resolveImportPath(imported.specifier, routerFilePath);
410
- if (!targetFile) continue;
411
- result = buildCombinedRouteMapWithSearch(
412
- targetFile,
413
- imported.exportedName,
414
- );
415
- } else {
416
- // Variable is defined in the same file
417
- result = buildCombinedRouteMapWithSearch(routerFilePath, urlsVarName);
599
+ const result = buildCombinedRouteMapForRouterFile(routerFilePath);
600
+ if (
601
+ Object.keys(result.routes).length === 0 &&
602
+ Object.keys(result.searchSchemas).length === 0
603
+ ) {
604
+ // Check if the file even has a createRouter call — if not, skip entirely.
605
+ // If it does, fall through to write an empty placeholder below.
606
+ let routerSource: string;
607
+ try {
608
+ routerSource = readFileSync(routerFilePath, "utf-8");
609
+ } catch {
610
+ continue;
611
+ }
612
+ if (!extractUrlsFromRouter(routerSource)) continue;
418
613
  }
419
614
 
420
- const routerBasename = pathBasename(routerFilePath).replace(
421
- /\.(tsx?|jsx?)$/,
422
- "",
423
- );
424
- const outPath = join(
425
- dirname(routerFilePath),
426
- `${routerBasename}.named-routes.gen.ts`,
427
- );
615
+ const outPath = genFileTsPath(routerFilePath);
428
616
  const existing = existsSync(outPath)
429
617
  ? readFileSync(outPath, "utf-8")
430
618
  : null;
431
619
 
432
- // When the static parser can't extract routes (e.g. callback-style urls()),
433
- // write an empty placeholder so the build-time transform's injected import
434
- // resolves. Runtime discovery will overwrite this with the real routes.
435
620
  if (Object.keys(result.routes).length === 0) {
436
621
  if (!existing) {
437
622
  const emptySource = generateRouteTypesSource({});
623
+ opts?.onWrite?.(outPath, emptySource);
438
624
  writeFileSync(outPath, emptySource);
439
625
  }
440
626
  continue;
@@ -446,11 +632,6 @@ export function writeCombinedRouteTypes(
446
632
  hasSearchSchemas ? result.searchSchemas : undefined,
447
633
  );
448
634
  if (existing !== source) {
449
- // On initial dev startup, don't overwrite a file from runtime discovery
450
- // (which has all dynamic routes) with a smaller set from the static
451
- // parser. The static parser can't see routes generated by Array.from()
452
- // or other dynamic code. During HMR (file watcher), always write so
453
- // newly added routes appear immediately.
454
635
  if (opts?.preserveIfLarger && existing) {
455
636
  const existingCount = countPublicRouteEntries(existing);
456
637
  const newCount = Object.keys(result.routes).filter(
@@ -460,9 +641,10 @@ export function writeCombinedRouteTypes(
460
641
  continue;
461
642
  }
462
643
  }
644
+ opts?.onWrite?.(outPath, source);
463
645
  writeFileSync(outPath, source);
464
646
  console.log(
465
- `[rsc-router] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
647
+ `[rango] Generated route types (${Object.keys(result.routes).length} routes) -> ${outPath}`,
466
648
  );
467
649
  }
468
650
  }
@@ -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") ||