@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,812 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import ts from "typescript";
4
+ import { getStringValue } from "./ast-helpers.js";
5
+ import { extractRoutesFromSource } from "./ast-route-extraction.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Unresolvable include diagnostics
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type UnresolvableReason =
12
+ | "factory-call"
13
+ | "dynamic-expression"
14
+ | "unresolvable-import"
15
+ | "file-not-found";
16
+
17
+ export interface UnresolvableInclude {
18
+ pathPrefix: string;
19
+ namePrefix: string | null;
20
+ reason: UnresolvableReason;
21
+ sourceFile: string;
22
+ detail: string;
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Per-scan memo
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Per-scan memo shared across the include-resolution recursion. Keys are
31
+ * absolute file paths; values cache the readFileSync result and the
32
+ * ts.SourceFile parsed from the FULL file source. A separate blockSources map
33
+ * caches parses of extracted sub-blocks (urls() call text), keyed by the exact
34
+ * block string, so the two extractors (routes + includes) for the same block
35
+ * share a single parse.
36
+ *
37
+ * The memo is purely a performance accelerator: same input -> same parse, so
38
+ * generated output is identical to the un-memoized path. A fresh memo is
39
+ * created per top-level scan entry, so stale file edits between scans are never
40
+ * served.
41
+ */
42
+ export interface ScanMemo {
43
+ files: Map<string, string>;
44
+ blockSourceFiles: Map<string, ts.SourceFile>;
45
+ }
46
+
47
+ export function createScanMemo(): ScanMemo {
48
+ return { files: new Map(), blockSourceFiles: new Map() };
49
+ }
50
+
51
+ function parseBlock(memo: ScanMemo | undefined, block: string): ts.SourceFile {
52
+ if (memo) {
53
+ const cached = memo.blockSourceFiles.get(block);
54
+ if (cached) return cached;
55
+ }
56
+ const sf = ts.createSourceFile(
57
+ "input.tsx",
58
+ block,
59
+ ts.ScriptTarget.Latest,
60
+ true,
61
+ ts.ScriptKind.TSX,
62
+ );
63
+ if (memo) memo.blockSourceFiles.set(block, sf);
64
+ return sf;
65
+ }
66
+
67
+ function readSourceMemoized(
68
+ memo: ScanMemo | undefined,
69
+ realPath: string,
70
+ ): string {
71
+ if (memo) {
72
+ const cached = memo.files.get(realPath);
73
+ if (cached !== undefined) return cached;
74
+ }
75
+ const source = readFileSync(realPath, "utf-8");
76
+ if (memo) memo.files.set(realPath, source);
77
+ return source;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // AST-based include() parsing
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function extractNamePrefixFromInclude(node: ts.CallExpression): string | null {
85
+ if (node.arguments.length >= 3) {
86
+ const thirdArg = node.arguments[2];
87
+ if (ts.isObjectLiteralExpression(thirdArg)) {
88
+ for (const prop of thirdArg.properties) {
89
+ if (!ts.isPropertyAssignment(prop)) continue;
90
+ const propName = ts.isIdentifier(prop.name) ? prop.name.text : null;
91
+ if (propName === "name") {
92
+ return getStringValue(prop.initializer);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * True when the thunk transforms its dynamic import via a `.then(cb)` whose
102
+ * callback selects a NAMED export other than `default` (e.g.
103
+ * `import("./x").then((m) => m.routes)`). The static resolver walks the module's
104
+ * `export default`, so such a selector would resolve the WRONG export — the
105
+ * caller must treat it as unresolvable instead of silently mis-resolving. Returns
106
+ * false for the supported shapes (no `.then`, `.then((m) => m)`,
107
+ * `.then((m) => m.default)`) and for any shape it cannot positively identify as a
108
+ * non-default member selection (those keep the existing resolve-via-default path).
109
+ */
110
+ function thenSelectsNonDefaultMember(expr: ts.Expression): boolean {
111
+ const findThenCall = (e: ts.Expression): ts.CallExpression | null => {
112
+ if (
113
+ ts.isCallExpression(e) &&
114
+ ts.isPropertyAccessExpression(e.expression) &&
115
+ e.expression.name.text === "then"
116
+ ) {
117
+ return e;
118
+ }
119
+ if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
120
+ return findThenCall(e.expression.expression);
121
+ }
122
+ if (ts.isPropertyAccessExpression(e)) return findThenCall(e.expression);
123
+ if (ts.isParenthesizedExpression(e)) return findThenCall(e.expression);
124
+ if (ts.isAwaitExpression(e)) return findThenCall(e.expression);
125
+ return null;
126
+ };
127
+
128
+ const thenCall = findThenCall(expr);
129
+ if (!thenCall || thenCall.arguments.length === 0) return false;
130
+ const cb = thenCall.arguments[0];
131
+ if (!ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb)) return false;
132
+
133
+ let ret: ts.Expression | undefined;
134
+ if (ts.isBlock(cb.body)) {
135
+ for (const stmt of cb.body.statements) {
136
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
137
+ ret = stmt.expression;
138
+ break;
139
+ }
140
+ }
141
+ } else {
142
+ ret = cb.body;
143
+ }
144
+ if (!ret) return false;
145
+
146
+ if (ts.isPropertyAccessExpression(ret)) {
147
+ return ret.name.text !== "default";
148
+ }
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Extract the module specifier from an async include thunk
154
+ * (`() => import("./mod")`). Handles arrow and function-expression thunks,
155
+ * concise or block bodies, and `import("./mod").then(...)` chains. Returns the
156
+ * specifier string, or null when the second arg is not a dynamic-import thunk
157
+ * (so the caller can fall through to identifier / factory-call / dynamic-expr
158
+ * classification).
159
+ */
160
+ function extractDynamicImportSpecifier(node: ts.Expression): string | null {
161
+ let body: ts.ConciseBody | undefined;
162
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
163
+ body = node.body;
164
+ }
165
+ if (!body) return null;
166
+
167
+ // Concise body is the expression itself; block body: first `return <expr>`.
168
+ let expr: ts.Expression | undefined;
169
+ if (ts.isBlock(body)) {
170
+ for (const stmt of body.statements) {
171
+ if (ts.isReturnStatement(stmt) && stmt.expression) {
172
+ expr = stmt.expression;
173
+ break;
174
+ }
175
+ }
176
+ } else {
177
+ expr = body;
178
+ }
179
+ if (!expr) return null;
180
+
181
+ // Descend `import("./mod").then(...)` / parenthesized / await wrappers down to
182
+ // the `import(...)` call itself.
183
+ const findImportCall = (e: ts.Expression): ts.CallExpression | null => {
184
+ if (
185
+ ts.isCallExpression(e) &&
186
+ e.expression.kind === ts.SyntaxKind.ImportKeyword
187
+ ) {
188
+ return e;
189
+ }
190
+ if (ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression)) {
191
+ return findImportCall(e.expression.expression);
192
+ }
193
+ if (ts.isPropertyAccessExpression(e)) return findImportCall(e.expression);
194
+ if (ts.isParenthesizedExpression(e)) return findImportCall(e.expression);
195
+ if (ts.isAwaitExpression(e)) return findImportCall(e.expression);
196
+ return null;
197
+ };
198
+
199
+ const importCall = findImportCall(expr);
200
+ if (!importCall || importCall.arguments.length === 0) return null;
201
+ // A `.then()` selecting a non-default named export can't be statically
202
+ // resolved to `export default`; return null so the caller emits a diagnostic
203
+ // rather than silently generating types for the wrong export.
204
+ if (thenSelectsNonDefaultMember(expr)) return null;
205
+ return getStringValue(importCall.arguments[0]);
206
+ }
207
+
208
+ /**
209
+ * Resolve a module's `export default` to either a same-file `urls()` variable
210
+ * name (`export default shopPatterns`) or the inline call block
211
+ * (`export default urls(...)`). Used to walk async include modules
212
+ * (`() => import("./mod")`), whose convention is `export default urls(...)`.
213
+ */
214
+ function resolveDefaultExportTarget(
215
+ code: string,
216
+ sourceFile: ts.SourceFile,
217
+ ): { variableName?: string; inlineBlock?: string } | null {
218
+ let result: { variableName?: string; inlineBlock?: string } | null = null;
219
+
220
+ function visit(node: ts.Node) {
221
+ if (result) return;
222
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
223
+ const expr = node.expression;
224
+ if (ts.isIdentifier(expr)) {
225
+ result = { variableName: expr.text };
226
+ } else if (ts.isCallExpression(expr)) {
227
+ const callee = expr.expression;
228
+ if (ts.isIdentifier(callee) && callee.text === "urls") {
229
+ result = { inlineBlock: expr.getText(sourceFile) };
230
+ }
231
+ }
232
+ return;
233
+ }
234
+ ts.forEachChild(node, visit);
235
+ }
236
+
237
+ visit(sourceFile);
238
+ return result;
239
+ }
240
+
241
+ /**
242
+ * Extract include() calls with diagnostics for unresolvable ones.
243
+ * Returns both resolved includes (identifier second args) and unresolvable
244
+ * includes (factory calls, etc.) with reasons.
245
+ */
246
+ export function extractIncludesWithDiagnostics(
247
+ code: string,
248
+ sourceFileArg?: ts.SourceFile,
249
+ ): {
250
+ resolved: Array<{
251
+ pathPrefix: string;
252
+ // Exactly one of variableName / moduleSpecifier is set. variableName is the
253
+ // classic `include("/api", apiUrls)` identifier form; moduleSpecifier is the
254
+ // async `include("/api", () => import("./api"))` form, carrying the imported
255
+ // module path so resolution can walk its `export default`.
256
+ variableName?: string;
257
+ moduleSpecifier?: string;
258
+ namePrefix: string | null;
259
+ }>;
260
+ unresolvable: Array<{
261
+ pathPrefix: string;
262
+ namePrefix: string | null;
263
+ reason: UnresolvableReason;
264
+ detail: string;
265
+ }>;
266
+ } {
267
+ // Reuse a caller-provided SourceFile (parsed once per scan) when given.
268
+ const sourceFile =
269
+ sourceFileArg ??
270
+ ts.createSourceFile(
271
+ "input.tsx",
272
+ code,
273
+ ts.ScriptTarget.Latest,
274
+ true,
275
+ ts.ScriptKind.TSX,
276
+ );
277
+ const resolved: Array<{
278
+ pathPrefix: string;
279
+ variableName?: string;
280
+ moduleSpecifier?: string;
281
+ namePrefix: string | null;
282
+ }> = [];
283
+ const unresolvable: Array<{
284
+ pathPrefix: string;
285
+ namePrefix: string | null;
286
+ reason: UnresolvableReason;
287
+ detail: string;
288
+ }> = [];
289
+
290
+ function visit(node: ts.Node) {
291
+ if (ts.isCallExpression(node)) {
292
+ const callee = node.expression;
293
+ if (ts.isIdentifier(callee) && callee.text === "include") {
294
+ if (node.arguments.length < 2) {
295
+ ts.forEachChild(node, visit);
296
+ return;
297
+ }
298
+
299
+ const pathPrefix = getStringValue(node.arguments[0]);
300
+ if (pathPrefix === null) {
301
+ ts.forEachChild(node, visit);
302
+ return;
303
+ }
304
+
305
+ const secondArg = node.arguments[1];
306
+ const namePrefix = extractNamePrefixFromInclude(node);
307
+
308
+ const dynamicImportSpec = extractDynamicImportSpecifier(secondArg);
309
+
310
+ if (ts.isIdentifier(secondArg)) {
311
+ resolved.push({
312
+ pathPrefix,
313
+ variableName: secondArg.text,
314
+ namePrefix,
315
+ });
316
+ } else if (dynamicImportSpec !== null) {
317
+ // Async include: `include("/api", () => import("./api"))`. Resolvable
318
+ // statically by walking the imported module's `export default`.
319
+ resolved.push({
320
+ pathPrefix,
321
+ moduleSpecifier: dynamicImportSpec,
322
+ namePrefix,
323
+ });
324
+ } else if (ts.isCallExpression(secondArg)) {
325
+ const callText = secondArg.expression.getText(sourceFile);
326
+ unresolvable.push({
327
+ pathPrefix,
328
+ namePrefix,
329
+ reason: "factory-call",
330
+ detail: `${callText}()`,
331
+ });
332
+ } else {
333
+ unresolvable.push({
334
+ pathPrefix,
335
+ namePrefix,
336
+ reason: "dynamic-expression",
337
+ detail: secondArg.getText(sourceFile),
338
+ });
339
+ }
340
+ }
341
+ }
342
+ ts.forEachChild(node, visit);
343
+ }
344
+
345
+ visit(sourceFile);
346
+ return { resolved, unresolvable };
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Import resolution
351
+ // ---------------------------------------------------------------------------
352
+
353
+ /**
354
+ * Find the import statement for a local variable name.
355
+ * Returns the import specifier and the exported name from the source module.
356
+ */
357
+ export function resolveImportedVariable(
358
+ code: string,
359
+ localName: string,
360
+ ): { specifier: string; exportedName: string } | null {
361
+ // Allow an optional leading default binding before the named-import brace so
362
+ // a combined `import Foo, { bar } from "..."` is matched (the named members
363
+ // are the only part we resolve; the default binding is skipped). Without the
364
+ // optional `(?:[\w$]+\s*,\s*)?` segment, the `Foo, ` prefix breaks the match
365
+ // and a legitimate static named import surfaces as `unresolvable-import`.
366
+ const importRegex =
367
+ /import\s*(?:[\w$]+\s*,\s*)?\{([^}]+)\}\s*from\s*["']([^"']+)["']/g;
368
+ let match;
369
+
370
+ while ((match = importRegex.exec(code)) !== null) {
371
+ const imports = match[1];
372
+ const specifier = match[2];
373
+
374
+ const parts = imports
375
+ .split(",")
376
+ .map((s) => s.trim())
377
+ .filter(Boolean);
378
+ for (const part of parts) {
379
+ const asMatch = part.match(/^(\w+)\s+as\s+(\w+)$/);
380
+ if (asMatch && asMatch[2] === localName)
381
+ return { specifier, exportedName: asMatch[1] };
382
+ if (part === localName) return { specifier, exportedName: localName };
383
+ }
384
+ }
385
+
386
+ return null;
387
+ }
388
+
389
+ /**
390
+ * Resolve an import specifier relative to the importing file.
391
+ * Strips .js/.mjs/.jsx extensions and tries .ts/.tsx/.js/.jsx candidates.
392
+ */
393
+ export function resolveImportPath(
394
+ importSpec: string,
395
+ fromFile: string,
396
+ ): string | null {
397
+ if (!importSpec.startsWith(".")) return null;
398
+
399
+ const dir = dirname(fromFile);
400
+ let base = importSpec;
401
+ if (base.endsWith(".js")) base = base.slice(0, -3);
402
+ else if (base.endsWith(".mjs")) base = base.slice(0, -4);
403
+ else if (base.endsWith(".jsx")) base = base.slice(0, -4);
404
+
405
+ const candidates = [
406
+ resolve(dir, base + ".ts"),
407
+ resolve(dir, base + ".tsx"),
408
+ resolve(dir, base + ".js"),
409
+ resolve(dir, base + ".jsx"),
410
+ resolve(dir, base + "/index.ts"),
411
+ resolve(dir, base + "/index.tsx"),
412
+ resolve(dir, base + "/index.js"),
413
+ resolve(dir, base + "/index.jsx"),
414
+ ];
415
+
416
+ for (const candidate of candidates) {
417
+ if (existsSync(candidate)) return candidate;
418
+ }
419
+ return null;
420
+ }
421
+
422
+ // ---------------------------------------------------------------------------
423
+ // urls() block extraction for same-file variables
424
+ // ---------------------------------------------------------------------------
425
+
426
+ /**
427
+ * Extract the source of a specific `const varName = urls(...)` call using
428
+ * the TypeScript AST. Returns the full text of the urls() call expression.
429
+ */
430
+ function extractUrlsBlockForVariable(
431
+ code: string,
432
+ varName: string,
433
+ sourceFileArg?: ts.SourceFile,
434
+ ): string | null {
435
+ // Reuse a caller-provided full-source SourceFile (parsed once per scan).
436
+ const sourceFile =
437
+ sourceFileArg ??
438
+ ts.createSourceFile(
439
+ "input.tsx",
440
+ code,
441
+ ts.ScriptTarget.Latest,
442
+ true,
443
+ ts.ScriptKind.TSX,
444
+ );
445
+ let result: string | null = null;
446
+
447
+ function visit(node: ts.Node) {
448
+ if (result) return;
449
+ if (
450
+ ts.isVariableDeclaration(node) &&
451
+ ts.isIdentifier(node.name) &&
452
+ node.name.text === varName &&
453
+ node.initializer &&
454
+ ts.isCallExpression(node.initializer)
455
+ ) {
456
+ const callee = node.initializer.expression;
457
+ if (ts.isIdentifier(callee) && callee.text === "urls") {
458
+ result = node.initializer.getText(sourceFile);
459
+ return;
460
+ }
461
+ }
462
+ ts.forEachChild(node, visit);
463
+ }
464
+
465
+ visit(sourceFile);
466
+ return result;
467
+ }
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // Combined route map building
471
+ // ---------------------------------------------------------------------------
472
+
473
+ function buildRouteMapFromBlock(
474
+ block: string,
475
+ fullSource: string,
476
+ filePath: string,
477
+ visited: Set<string>,
478
+ searchSchemasOut?: Record<string, Record<string, string>>,
479
+ diagnosticsOut?: UnresolvableInclude[],
480
+ memo?: ScanMemo,
481
+ ): Record<string, string> {
482
+ const routeMap: Record<string, string> = {};
483
+
484
+ // Parse the block once and share the SourceFile between both extractors.
485
+ const blockSourceFile = parseBlock(memo, block);
486
+
487
+ // Extract local path() routes
488
+ const localRoutes = extractRoutesFromSource(block, blockSourceFile);
489
+ for (const { name, pattern, search } of localRoutes) {
490
+ routeMap[name] = pattern;
491
+ if (search && searchSchemasOut) {
492
+ searchSchemasOut[name] = search;
493
+ }
494
+ }
495
+
496
+ // Extract include() calls with diagnostics for unresolvable ones
497
+ const { resolved: includes, unresolvable } = extractIncludesWithDiagnostics(
498
+ block,
499
+ blockSourceFile,
500
+ );
501
+
502
+ if (diagnosticsOut) {
503
+ for (const entry of unresolvable) {
504
+ diagnosticsOut.push({ ...entry, sourceFile: filePath });
505
+ }
506
+ }
507
+
508
+ for (const inc of includes) {
509
+ const { pathPrefix, namePrefix } = inc;
510
+ let childResult: {
511
+ routes: Record<string, string>;
512
+ searchSchemas: Record<string, Record<string, string>>;
513
+ };
514
+
515
+ if (inc.moduleSpecifier) {
516
+ // Async include `() => import("./mod")`: resolve the imported module's
517
+ // `export default` (convention: `export default urls(...)`), then recurse
518
+ // into it exactly like an identifier include. Nested include()s inside the
519
+ // async module — eager or async — resolve through the same recursion.
520
+ const targetFile = resolveImportPath(inc.moduleSpecifier, filePath);
521
+ if (!targetFile) {
522
+ diagnosticsOut?.push({
523
+ pathPrefix,
524
+ namePrefix,
525
+ reason: "file-not-found",
526
+ sourceFile: filePath,
527
+ detail: `import("${inc.moduleSpecifier}") resolved to no file`,
528
+ });
529
+ continue;
530
+ }
531
+ let targetSource: string;
532
+ try {
533
+ targetSource = readSourceMemoized(memo, resolve(targetFile));
534
+ } catch (err) {
535
+ // Every other failure path in this function emits a diagnostic; a bare
536
+ // `continue` here would drop the group from generated types with no
537
+ // signal at all. Emit one like the siblings do.
538
+ diagnosticsOut?.push({
539
+ pathPrefix,
540
+ namePrefix,
541
+ reason: "file-not-found",
542
+ sourceFile: filePath,
543
+ detail: `import("${inc.moduleSpecifier}") resolved to "${targetFile}" but reading it failed: ${(err as Error)?.message ?? String(err)}`,
544
+ });
545
+ continue;
546
+ }
547
+ const def = resolveDefaultExportTarget(
548
+ targetSource,
549
+ parseBlock(memo, targetSource),
550
+ );
551
+ if (!def) {
552
+ diagnosticsOut?.push({
553
+ pathPrefix,
554
+ namePrefix,
555
+ reason: "unresolvable-import",
556
+ sourceFile: filePath,
557
+ detail: `import("${inc.moduleSpecifier}") has no resolvable \`export default urls(...)\``,
558
+ });
559
+ continue;
560
+ }
561
+ if (def.inlineBlock) {
562
+ // `export default urls(...)` — recurse into the inline call block.
563
+ childResult = buildCombinedRouteMapWithSearch(
564
+ targetFile,
565
+ undefined,
566
+ visited,
567
+ diagnosticsOut,
568
+ def.inlineBlock,
569
+ memo,
570
+ );
571
+ } else {
572
+ // `export default <name>`. That name may be a same-file
573
+ // `const name = urls(...)` OR itself re-exported from an import
574
+ // (`import { name } from "./x"; export default name`). The shared helper
575
+ // resolves the import chain first, then falls back to same-file
576
+ // extraction — without this a re-exported default silently yields empty
577
+ // routes. Resolution scope is the imported module (targetFile); the
578
+ // diagnostic is still attributed to the file with the async include.
579
+ const variableName = def.variableName!;
580
+ const resolved = resolveIncludedVariable({
581
+ variableName,
582
+ resolutionFile: targetFile,
583
+ resolutionSource: targetSource,
584
+ reportFile: filePath,
585
+ memo,
586
+ visited,
587
+ diagnosticsOut,
588
+ pathPrefix,
589
+ namePrefix,
590
+ fileNotFoundDetail: (specifier) =>
591
+ `import("${inc.moduleSpecifier}") default re-exports "${variableName}" from "${specifier}", which resolved to no file`,
592
+ notFoundDetail: () =>
593
+ `import("${inc.moduleSpecifier}") default "${variableName}" not found in its imports or same-file scope`,
594
+ });
595
+ if (!resolved) continue;
596
+ childResult = resolved;
597
+ }
598
+ } else if (inc.variableName) {
599
+ // Identifier include `include(prefix, patterns)`: resolve `patterns`
600
+ // (imported or same-file) through the same shared helper as the async
601
+ // `export default <name>` form above.
602
+ const variableName = inc.variableName;
603
+ const resolved = resolveIncludedVariable({
604
+ variableName,
605
+ resolutionFile: filePath,
606
+ resolutionSource: fullSource,
607
+ reportFile: filePath,
608
+ memo,
609
+ visited,
610
+ diagnosticsOut,
611
+ pathPrefix,
612
+ namePrefix,
613
+ fileNotFoundDetail: (specifier) =>
614
+ `import "${specifier}" resolved to no file`,
615
+ notFoundDetail: () =>
616
+ `variable "${variableName}" not found in imports or same-file scope`,
617
+ });
618
+ if (!resolved) continue;
619
+ childResult = resolved;
620
+ } else {
621
+ continue;
622
+ }
623
+
624
+ // Includes without a name keep their child names private to the mounted
625
+ // module. They remain active at runtime via an internal scope prefix, but
626
+ // they are intentionally omitted from generated public route maps.
627
+ if (namePrefix === null) {
628
+ continue;
629
+ }
630
+
631
+ // Apply prefixes
632
+ for (const [name, pattern] of Object.entries(childResult.routes)) {
633
+ const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
634
+ let prefixedPattern: string;
635
+ if (pattern === "/") {
636
+ prefixedPattern = pathPrefix || "/";
637
+ } else if (pathPrefix.endsWith("/") && pattern.startsWith("/")) {
638
+ prefixedPattern = pathPrefix + pattern.slice(1);
639
+ } else {
640
+ prefixedPattern = pathPrefix + pattern;
641
+ }
642
+ routeMap[prefixedName] = prefixedPattern;
643
+ // Propagate search schemas with prefix
644
+ if (childResult.searchSchemas[name] && searchSchemasOut) {
645
+ searchSchemasOut[prefixedName] = childResult.searchSchemas[name];
646
+ }
647
+ }
648
+ }
649
+
650
+ return routeMap;
651
+ }
652
+
653
+ /**
654
+ * Resolve an include's target VARIABLE to its child route map. The variable may
655
+ * be imported (`import { name } from "./x"`) or defined in the same file
656
+ * (`const name = urls(...)`). Shared by the identifier include branch and the
657
+ * async-include `export default <name>` branch so a future import-resolution fix
658
+ * cannot silently miss one form. Returns null (with a diagnostic pushed) when
659
+ * the variable is unresolvable — the caller should skip the include.
660
+ *
661
+ * `resolutionFile`/`resolutionSource` is the module whose import + same-file
662
+ * scope is searched; `reportFile` is the file the diagnostic is attributed to
663
+ * (the one containing the include() call), which differs for async includes.
664
+ */
665
+ function resolveIncludedVariable(opts: {
666
+ variableName: string;
667
+ resolutionFile: string;
668
+ resolutionSource: string;
669
+ reportFile: string;
670
+ memo: ScanMemo | undefined;
671
+ visited: Set<string> | undefined;
672
+ diagnosticsOut: UnresolvableInclude[] | undefined;
673
+ pathPrefix: string;
674
+ namePrefix: string | null;
675
+ fileNotFoundDetail: (specifier: string) => string;
676
+ notFoundDetail: () => string;
677
+ }): {
678
+ routes: Record<string, string>;
679
+ searchSchemas: Record<string, Record<string, string>>;
680
+ } | null {
681
+ const {
682
+ variableName,
683
+ resolutionFile,
684
+ resolutionSource,
685
+ reportFile,
686
+ memo,
687
+ visited,
688
+ diagnosticsOut,
689
+ pathPrefix,
690
+ namePrefix,
691
+ } = opts;
692
+ const imported = resolveImportedVariable(resolutionSource, variableName);
693
+ if (imported) {
694
+ const targetFile = resolveImportPath(imported.specifier, resolutionFile);
695
+ if (!targetFile) {
696
+ diagnosticsOut?.push({
697
+ pathPrefix,
698
+ namePrefix,
699
+ reason: "file-not-found",
700
+ sourceFile: reportFile,
701
+ detail: opts.fileNotFoundDetail(imported.specifier),
702
+ });
703
+ return null;
704
+ }
705
+ return buildCombinedRouteMapWithSearch(
706
+ targetFile,
707
+ imported.exportedName,
708
+ visited,
709
+ diagnosticsOut,
710
+ undefined,
711
+ memo,
712
+ );
713
+ }
714
+ // Not imported: it must be a same-file `const name = urls(...)`. Confirm it
715
+ // exists so an undefined name emits a diagnostic instead of empty routes.
716
+ const sameFileBlock = extractUrlsBlockForVariable(
717
+ resolutionSource,
718
+ variableName,
719
+ parseBlock(memo, resolutionSource),
720
+ );
721
+ if (!sameFileBlock) {
722
+ diagnosticsOut?.push({
723
+ pathPrefix,
724
+ namePrefix,
725
+ reason: "unresolvable-import",
726
+ sourceFile: reportFile,
727
+ detail: opts.notFoundDetail(),
728
+ });
729
+ return null;
730
+ }
731
+ return buildCombinedRouteMapWithSearch(
732
+ resolutionFile,
733
+ variableName,
734
+ visited,
735
+ diagnosticsOut,
736
+ undefined,
737
+ memo,
738
+ );
739
+ }
740
+
741
+ /**
742
+ * Build route map and search schemas together.
743
+ * Internal helper used by the include resolution path.
744
+ *
745
+ * @param inlineBlock - Optional pre-extracted code block (e.g. from an inline
746
+ * builder function). When provided, variableName is ignored and the block
747
+ * is parsed directly for path()/include() calls.
748
+ * @param memo - Per-scan readFileSync/parse memo. A fresh one is created at the
749
+ * top-level entry and threaded through the recursion so each file is read and
750
+ * parsed once per scan. Behavior-preserving (same input -> same output).
751
+ */
752
+ export function buildCombinedRouteMapWithSearch(
753
+ filePath: string,
754
+ variableName?: string,
755
+ visited?: Set<string>,
756
+ diagnosticsOut?: UnresolvableInclude[],
757
+ inlineBlock?: string,
758
+ memo?: ScanMemo,
759
+ ): {
760
+ routes: Record<string, string>;
761
+ searchSchemas: Record<string, Record<string, string>>;
762
+ } {
763
+ visited = visited ?? new Set();
764
+ memo = memo ?? createScanMemo();
765
+ const realPath = resolve(filePath);
766
+ const key = variableName ? `${realPath}:${variableName}` : realPath;
767
+ if (visited.has(key)) {
768
+ console.warn(`[rango] Circular include detected, skipping: ${key}`);
769
+ return { routes: {}, searchSchemas: {} };
770
+ }
771
+ visited.add(key);
772
+
773
+ let source: string;
774
+ try {
775
+ source = readSourceMemoized(memo, realPath);
776
+ } catch {
777
+ return { routes: {}, searchSchemas: {} };
778
+ }
779
+
780
+ let block: string;
781
+ if (inlineBlock) {
782
+ block = inlineBlock;
783
+ } else if (variableName) {
784
+ const extracted = extractUrlsBlockForVariable(
785
+ source,
786
+ variableName,
787
+ parseBlock(memo, source),
788
+ );
789
+ if (!extracted) return { routes: {}, searchSchemas: {} };
790
+ block = extracted;
791
+ } else {
792
+ block = source;
793
+ }
794
+
795
+ const searchSchemas: Record<string, Record<string, string>> = {};
796
+ const routes = buildRouteMapFromBlock(
797
+ block,
798
+ source,
799
+ realPath,
800
+ visited,
801
+ searchSchemas,
802
+ diagnosticsOut,
803
+ memo,
804
+ );
805
+
806
+ // Remove from visited so sibling branches can include the same variable
807
+ // without false circular-include detection. Only ancestors in the current
808
+ // recursion path should trigger the cycle guard.
809
+ visited.delete(key);
810
+
811
+ return { routes, searchSchemas };
812
+ }