@lovrozagar/flare 0.1.0

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 (387) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/package.json +146 -0
  4. package/scripts/download-fonts.ts +136 -0
  5. package/scripts/populate-fonts.ts +721 -0
  6. package/src/await.ts +2 -0
  7. package/src/boundaries/index.ts +99 -0
  8. package/src/broadcast/channel.ts +65 -0
  9. package/src/broadcast/context.ts +12 -0
  10. package/src/broadcast/index.ts +75 -0
  11. package/src/broadcast/provider.tsx +7 -0
  12. package/src/build-location.ts +2 -0
  13. package/src/caches/index.ts +384 -0
  14. package/src/client/index.ts +75 -0
  15. package/src/client.tsx +2 -0
  16. package/src/codegen/index.ts +41 -0
  17. package/src/components/await.tsx +140 -0
  18. package/src/components/dev-error-overlay.tsx +154 -0
  19. package/src/components/direction-script.tsx +22 -0
  20. package/src/components/index.ts +150 -0
  21. package/src/components/locale-script.tsx +26 -0
  22. package/src/components/reset-css.tsx +14 -0
  23. package/src/components/ssr-context.tsx +37 -0
  24. package/src/components/theme-script.tsx +22 -0
  25. package/src/components/view-transition-css.tsx +21 -0
  26. package/src/config/index.ts +84 -0
  27. package/src/create-client-only-fn.ts +1 -0
  28. package/src/create-isomorphic-fn.ts +2 -0
  29. package/src/create-layout.ts +1 -0
  30. package/src/create-page.ts +1 -0
  31. package/src/create-path-segment.ts +1 -0
  32. package/src/create-query-client-getter.ts +2 -0
  33. package/src/create-root-layout.ts +1 -0
  34. package/src/create-router.ts +8 -0
  35. package/src/create-server-only-fn.ts +1 -0
  36. package/src/dedupe/index.ts +151 -0
  37. package/src/defer/index.ts +82 -0
  38. package/src/direction/index.tsx +163 -0
  39. package/src/direction.ts +3 -0
  40. package/src/duration/index.ts +18 -0
  41. package/src/env-fn/index.ts +101 -0
  42. package/src/env-fn.ts +2 -0
  43. package/src/errors/index.ts +166 -0
  44. package/src/fonts/abeezee.ts +50 -0
  45. package/src/fonts/abel.ts +26 -0
  46. package/src/fonts/abril-fatface.ts +34 -0
  47. package/src/fonts/acme.ts +26 -0
  48. package/src/fonts/advent-pro.ts +92 -0
  49. package/src/fonts/alegreya-sans.ts +228 -0
  50. package/src/fonts/alegreya.ts +228 -0
  51. package/src/fonts/alex-brush.ts +42 -0
  52. package/src/fonts/alfa-slab-one.ts +42 -0
  53. package/src/fonts/alice.ts +48 -0
  54. package/src/fonts/amatic-sc.ts +94 -0
  55. package/src/fonts/amiri.ts +114 -0
  56. package/src/fonts/antic-slab.ts +26 -0
  57. package/src/fonts/anton.ts +42 -0
  58. package/src/fonts/archivo-black.ts +34 -0
  59. package/src/fonts/archivo-narrow.ts +114 -0
  60. package/src/fonts/archivo.ts +66 -0
  61. package/src/fonts/arimo.ts +256 -0
  62. package/src/fonts/arvo.ts +50 -0
  63. package/src/fonts/asap.ts +66 -0
  64. package/src/fonts/assistant.ts +64 -0
  65. package/src/fonts/baloo-2.ts +82 -0
  66. package/src/fonts/barlow-condensed.ts +114 -0
  67. package/src/fonts/barlow-semi-condensed.ts +114 -0
  68. package/src/fonts/barlow.ts +114 -0
  69. package/src/fonts/be-vietnam-pro.ts +114 -0
  70. package/src/fonts/bebas-neue.ts +34 -0
  71. package/src/fonts/besley.ts +82 -0
  72. package/src/fonts/biorhyme.ts +50 -0
  73. package/src/fonts/bitter.ts +94 -0
  74. package/src/fonts/black-ops-one.ts +49 -0
  75. package/src/fonts/bree-serif.ts +34 -0
  76. package/src/fonts/bricolage-grotesque.ts +66 -0
  77. package/src/fonts/cabin.ts +114 -0
  78. package/src/fonts/cairo.ts +66 -0
  79. package/src/fonts/cardo.ts +193 -0
  80. package/src/fonts/catamaran.ts +41 -0
  81. package/src/fonts/caveat.ts +78 -0
  82. package/src/fonts/chakra-petch.ts +142 -0
  83. package/src/fonts/chivo.ts +66 -0
  84. package/src/fonts/cinzel.ts +50 -0
  85. package/src/fonts/comfortaa.ts +109 -0
  86. package/src/fonts/commissioner.ts +64 -0
  87. package/src/fonts/component.tsx +27 -0
  88. package/src/fonts/cormorant-garamond.ts +171 -0
  89. package/src/fonts/cormorant.ts +170 -0
  90. package/src/fonts/courgette.ts +34 -0
  91. package/src/fonts/courier-prime.ts +82 -0
  92. package/src/fonts/create-font.ts +47 -0
  93. package/src/fonts/create-registry-font.ts +40 -0
  94. package/src/fonts/crete-round.ts +50 -0
  95. package/src/fonts/crimson-text.ts +114 -0
  96. package/src/fonts/css.ts +86 -0
  97. package/src/fonts/dancing-script.ts +66 -0
  98. package/src/fonts/dm-mono.ts +50 -0
  99. package/src/fonts/dm-sans.ts +50 -0
  100. package/src/fonts/dm-serif-display.ts +50 -0
  101. package/src/fonts/domine.ts +50 -0
  102. package/src/fonts/dosis.ts +66 -0
  103. package/src/fonts/eb-garamond.ts +228 -0
  104. package/src/fonts/eczar.ts +94 -0
  105. package/src/fonts/encode-sans.ts +42 -0
  106. package/src/fonts/epilogue.ts +66 -0
  107. package/src/fonts/exo-2.ts +94 -0
  108. package/src/fonts/exo.ts +66 -0
  109. package/src/fonts/figtree.ts +82 -0
  110. package/src/fonts/fira-code.ts +121 -0
  111. package/src/fonts/fira-sans-condensed.ts +228 -0
  112. package/src/fonts/fira-sans.ts +228 -0
  113. package/src/fonts/fjalla-one.ts +49 -0
  114. package/src/fonts/font-urls.gen.json +1962 -0
  115. package/src/fonts/fraunces.ts +66 -0
  116. package/src/fonts/fredoka.ts +64 -0
  117. package/src/fonts/gelasio.ts +114 -0
  118. package/src/fonts/gloria-hallelujah.ts +34 -0
  119. package/src/fonts/gothic-a1.ts +124 -0
  120. package/src/fonts/great-vibes.ts +64 -0
  121. package/src/fonts/gudea.ts +66 -0
  122. package/src/fonts/heebo.ts +57 -0
  123. package/src/fonts/hind-siliguri.ts +66 -0
  124. package/src/fonts/hind.ts +66 -0
  125. package/src/fonts/ibm-plex-mono.ts +172 -0
  126. package/src/fonts/ibm-plex-sans-arabic.ts +80 -0
  127. package/src/fonts/ibm-plex-sans.ts +199 -0
  128. package/src/fonts/ibm-plex-serif.ts +171 -0
  129. package/src/fonts/inconsolata.ts +66 -0
  130. package/src/fonts/index.ts +4 -0
  131. package/src/fonts/indie-flower.ts +34 -0
  132. package/src/fonts/instrument-sans.ts +82 -0
  133. package/src/fonts/instrument-serif.ts +50 -0
  134. package/src/fonts/inter-display.ts +52 -0
  135. package/src/fonts/inter-tight.ts +124 -0
  136. package/src/fonts/inter.ts +123 -0
  137. package/src/fonts/jetbrains-mono.ts +199 -0
  138. package/src/fonts/josefin-sans.ts +114 -0
  139. package/src/fonts/josefin-slab.ts +50 -0
  140. package/src/fonts/jost.ts +64 -0
  141. package/src/fonts/kalam.ts +66 -0
  142. package/src/fonts/kanit.ts +142 -0
  143. package/src/fonts/karla.ts +82 -0
  144. package/src/fonts/khand.ts +66 -0
  145. package/src/fonts/lato.ts +82 -0
  146. package/src/fonts/league-spartan.ts +42 -0
  147. package/src/fonts/lexend-deca.ts +42 -0
  148. package/src/fonts/lexend.ts +42 -0
  149. package/src/fonts/libre-baskerville.ts +82 -0
  150. package/src/fonts/libre-franklin.ts +95 -0
  151. package/src/fonts/lilita-one.ts +34 -0
  152. package/src/fonts/lobster-two.ts +50 -0
  153. package/src/fonts/lobster.ts +56 -0
  154. package/src/fonts/lora.ts +235 -0
  155. package/src/fonts/m-plus-rounded-1c.ts +138 -0
  156. package/src/fonts/manrope.ts +109 -0
  157. package/src/fonts/maven-pro.ts +66 -0
  158. package/src/fonts/merriweather-sans.ts +142 -0
  159. package/src/fonts/merriweather.ts +171 -0
  160. package/src/fonts/montserrat-alternates.ts +171 -0
  161. package/src/fonts/montserrat.ts +94 -0
  162. package/src/fonts/mukta.ts +66 -0
  163. package/src/fonts/mulish.ts +170 -0
  164. package/src/fonts/nanum-gothic.ts +34 -0
  165. package/src/fonts/nanum-myeongjo.ts +34 -0
  166. package/src/fonts/neuton.ts +66 -0
  167. package/src/fonts/newsreader.ts +114 -0
  168. package/src/fonts/noticia-text.ts +114 -0
  169. package/src/fonts/noto-sans-arabic.ts +58 -0
  170. package/src/fonts/noto-sans-hk.ts +49 -0
  171. package/src/fonts/noto-sans-jp.ts +49 -0
  172. package/src/fonts/noto-sans-kr.ts +49 -0
  173. package/src/fonts/noto-sans-sc.ts +49 -0
  174. package/src/fonts/noto-sans-tc.ts +49 -0
  175. package/src/fonts/noto-sans.ts +140 -0
  176. package/src/fonts/noto-serif-jp.ts +80 -0
  177. package/src/fonts/noto-serif.ts +140 -0
  178. package/src/fonts/nunito-sans.ts +170 -0
  179. package/src/fonts/nunito.ts +170 -0
  180. package/src/fonts/old-standard-tt.ts +133 -0
  181. package/src/fonts/onest.ts +48 -0
  182. package/src/fonts/open-sans.ts +340 -0
  183. package/src/fonts/orbitron.ts +34 -0
  184. package/src/fonts/oswald.ts +94 -0
  185. package/src/fonts/outfit.ts +34 -0
  186. package/src/fonts/overpass.ts +94 -0
  187. package/src/fonts/oxygen.ts +50 -0
  188. package/src/fonts/pacifico.ts +56 -0
  189. package/src/fonts/passion-one.ts +50 -0
  190. package/src/fonts/pathway-gothic-one.ts +34 -0
  191. package/src/fonts/permanent-marker.ts +26 -0
  192. package/src/fonts/philosopher.ts +172 -0
  193. package/src/fonts/play.ts +109 -0
  194. package/src/fonts/playfair-display.ts +142 -0
  195. package/src/fonts/plus-jakarta-sans.ts +142 -0
  196. package/src/fonts/poppins.ts +114 -0
  197. package/src/fonts/prompt.ts +142 -0
  198. package/src/fonts/pt-sans-narrow.ts +78 -0
  199. package/src/fonts/pt-sans.ts +138 -0
  200. package/src/fonts/pt-serif.ts +138 -0
  201. package/src/fonts/public-sans.ts +66 -0
  202. package/src/fonts/questrial.ts +42 -0
  203. package/src/fonts/quicksand.ts +66 -0
  204. package/src/fonts/rajdhani.ts +66 -0
  205. package/src/fonts/raleway.ts +94 -0
  206. package/src/fonts/recursive.ts +80 -0
  207. package/src/fonts/red-hat-display.ts +82 -0
  208. package/src/fonts/red-rose.ts +66 -0
  209. package/src/fonts/registry.gen.ts +173 -0
  210. package/src/fonts/righteous.ts +34 -0
  211. package/src/fonts/roboto-condensed.ts +124 -0
  212. package/src/fonts/roboto-flex.ts +64 -0
  213. package/src/fonts/roboto-mono.ts +199 -0
  214. package/src/fonts/roboto-slab.ts +72 -0
  215. package/src/fonts/roboto.ts +156 -0
  216. package/src/fonts/rokkitt.ts +66 -0
  217. package/src/fonts/rubik.ts +199 -0
  218. package/src/fonts/sacramento.ts +34 -0
  219. package/src/fonts/saira.ts +66 -0
  220. package/src/fonts/sarabun.ts +142 -0
  221. package/src/fonts/satisfy.ts +26 -0
  222. package/src/fonts/sawarabi-mincho.ts +41 -0
  223. package/src/fonts/shadows-into-light.ts +34 -0
  224. package/src/fonts/signika-negative.ts +66 -0
  225. package/src/fonts/signika.ts +66 -0
  226. package/src/fonts/slabo-27px.ts +34 -0
  227. package/src/fonts/sora.ts +50 -0
  228. package/src/fonts/source-code-pro.ts +228 -0
  229. package/src/fonts/source-sans-3.ts +228 -0
  230. package/src/fonts/source-serif-4.ts +199 -0
  231. package/src/fonts/space-grotesk.ts +66 -0
  232. package/src/fonts/space-mono.ts +114 -0
  233. package/src/fonts/spectral.ts +170 -0
  234. package/src/fonts/tajawal.ts +50 -0
  235. package/src/fonts/teko.ts +66 -0
  236. package/src/fonts/tenor-sans.ts +41 -0
  237. package/src/fonts/tinos.ts +256 -0
  238. package/src/fonts/titillium-web.ts +82 -0
  239. package/src/fonts/types.ts +43 -0
  240. package/src/fonts/ubuntu-condensed.ts +63 -0
  241. package/src/fonts/ubuntu-mono.ts +195 -0
  242. package/src/fonts/ubuntu.ts +195 -0
  243. package/src/fonts/unbounded.ts +94 -0
  244. package/src/fonts/unicode-ranges.gen.ts +28 -0
  245. package/src/fonts/urbanist.ts +50 -0
  246. package/src/fonts/varela-round.ts +49 -0
  247. package/src/fonts/vollkorn.ts +199 -0
  248. package/src/fonts/work-sans.ts +66 -0
  249. package/src/fonts/yanone-kaffeesatz.ts +128 -0
  250. package/src/fonts/yantramanav.ts +66 -0
  251. package/src/fonts/yellowtail.ts +34 -0
  252. package/src/fonts/young-serif.ts +34 -0
  253. package/src/fonts/zen-kaku-gothic-new.ts +64 -0
  254. package/src/fonts/zilla-slab.ts +82 -0
  255. package/src/form/index.tsx +205 -0
  256. package/src/generators/index.ts +1589 -0
  257. package/src/head-client/index.ts +649 -0
  258. package/src/history/index.ts +210 -0
  259. package/src/hooks.ts +61 -0
  260. package/src/hydrate/index.tsx +381 -0
  261. package/src/hydration/index.ts +187 -0
  262. package/src/i18n/index.ts +250 -0
  263. package/src/i18n/jsx.tsx +42 -0
  264. package/src/i18n/message-format.ts +174 -0
  265. package/src/image/index.tsx +473 -0
  266. package/src/image.ts +12 -0
  267. package/src/intercept-outlet/index.tsx +18 -0
  268. package/src/intercept-outlet.ts +2 -0
  269. package/src/internal/index.ts +143 -0
  270. package/src/internal/once-idle.ts +24 -0
  271. package/src/internal.ts +12 -0
  272. package/src/lazy/index.tsx +205 -0
  273. package/src/link/index.tsx +367 -0
  274. package/src/loader-pipeline/index.ts +639 -0
  275. package/src/locale/index.tsx +137 -0
  276. package/src/locale.ts +3 -0
  277. package/src/logger.ts +22 -0
  278. package/src/middleware/builtins/api-proxy.ts +64 -0
  279. package/src/middleware/builtins/cdn-proxy.ts +58 -0
  280. package/src/middleware/builtins/i18n.ts +256 -0
  281. package/src/middleware/builtins/keepalive.ts +24 -0
  282. package/src/middleware/builtins/markdown-negotiation.ts +141 -0
  283. package/src/middleware/builtins/static-assets.ts +32 -0
  284. package/src/middleware/index.ts +223 -0
  285. package/src/module-graph/index.ts +181 -0
  286. package/src/mount/index.ts +146 -0
  287. package/src/navigate.ts +1 -0
  288. package/src/navigation/index.ts +1278 -0
  289. package/src/navigation/types.ts +59 -0
  290. package/src/navigation-progress/index.tsx +247 -0
  291. package/src/navigation-progress.ts +1 -0
  292. package/src/ndjson-client/index.ts +376 -0
  293. package/src/ndjson-server/index.ts +284 -0
  294. package/src/outlet/index.tsx +633 -0
  295. package/src/outlet/types.ts +243 -0
  296. package/src/plugins/css-transform.ts +70 -0
  297. package/src/plugins/dev-cdn-cache.ts +454 -0
  298. package/src/plugins/dev-dashboard/client.tsx +2603 -0
  299. package/src/plugins/dev-dashboard/match-route-tree.ts +159 -0
  300. package/src/plugins/dev-dashboard/plugin.ts +404 -0
  301. package/src/plugins/dev-prerender.ts +168 -0
  302. package/src/plugins/dev-server.ts +327 -0
  303. package/src/plugins/generate-watch.ts +32 -0
  304. package/src/plugins/image-plugin.ts +171 -0
  305. package/src/plugins/index.ts +445 -0
  306. package/src/plugins/options.ts +37 -0
  307. package/src/plugins/parse-utils.ts +59 -0
  308. package/src/plugins/prerender-plugin.ts +144 -0
  309. package/src/plugins/purge.ts +242 -0
  310. package/src/plugins/server-fn.ts +641 -0
  311. package/src/plugins/service-worker.ts +225 -0
  312. package/src/plugins/solid-jsx.d.ts +12 -0
  313. package/src/plugins/sx-ast/atomic.ts +166 -0
  314. package/src/plugins/sx-ast/evaluator.ts +235 -0
  315. package/src/plugins/sx-ast/index.ts +372 -0
  316. package/src/plugins/sx-ast/parser.ts +22 -0
  317. package/src/plugins/sx-ast/rewrite.ts +763 -0
  318. package/src/plugins/sx.d.ts +14 -0
  319. package/src/plugins/tw-compile.ts +240 -0
  320. package/src/plugins/types.ts +41 -0
  321. package/src/plugins/virtual.d.ts +110 -0
  322. package/src/plugins/virtual.ts +184 -0
  323. package/src/preload/index.ts +77 -0
  324. package/src/prerender/index.ts +431 -0
  325. package/src/query-client/index.tsx +168 -0
  326. package/src/registry/index.ts +88 -0
  327. package/src/reset-css.ts +1 -0
  328. package/src/revalidation/index.ts +84 -0
  329. package/src/rewrite/index.ts +83 -0
  330. package/src/route-builder/create-layout.ts +655 -0
  331. package/src/route-builder/create-page.ts +806 -0
  332. package/src/route-builder/create-path-segment.ts +34 -0
  333. package/src/route-builder/create-root-layout.ts +608 -0
  334. package/src/route-builder/index.ts +49 -0
  335. package/src/route-builder/register.ts +242 -0
  336. package/src/route-builder/types.ts +294 -0
  337. package/src/router-config/index.ts +91 -0
  338. package/src/router-primitives/index.ts +25 -0
  339. package/src/router-primitives/location.ts +29 -0
  340. package/src/router-primitives/match-id.ts +111 -0
  341. package/src/router-primitives/paths.ts +109 -0
  342. package/src/router-primitives/tree.ts +243 -0
  343. package/src/router-primitives/types.ts +74 -0
  344. package/src/router.ts +31 -0
  345. package/src/search-engine/bing.ts +32 -0
  346. package/src/search-engine/google-jwt.ts +95 -0
  347. package/src/search-engine/google.ts +139 -0
  348. package/src/search-engine/index-now.ts +64 -0
  349. package/src/search-engine/index.ts +12 -0
  350. package/src/security/index.ts +230 -0
  351. package/src/server/index.ts +286 -0
  352. package/src/server-context/browser.ts +113 -0
  353. package/src/server-context/index.ts +217 -0
  354. package/src/server-fn/index.ts +710 -0
  355. package/src/server-fn-query.ts +169 -0
  356. package/src/server-handler/etag.ts +51 -0
  357. package/src/server-handler/headers.ts +5 -0
  358. package/src/server-handler/index.ts +1664 -0
  359. package/src/server-handler/types.ts +24 -0
  360. package/src/server-handler/validate-static-params.ts +130 -0
  361. package/src/server.ts +2 -0
  362. package/src/service-worker/template.ts +153 -0
  363. package/src/service-worker-hook/index.ts +70 -0
  364. package/src/sitemap/index.ts +231 -0
  365. package/src/ssr/critical-css.ts +150 -0
  366. package/src/ssr/head-prefix.ts +65 -0
  367. package/src/ssr/head.ts +259 -0
  368. package/src/ssr/index.tsx +879 -0
  369. package/src/state-parser/index.ts +266 -0
  370. package/src/store/filesystem.ts +131 -0
  371. package/src/store/index.ts +29 -0
  372. package/src/styles/index.ts +1200 -0
  373. package/src/styles/sx-types.ts +11 -0
  374. package/src/testing/index.ts +251 -0
  375. package/src/testing.ts +2 -0
  376. package/src/theme/index.tsx +232 -0
  377. package/src/theme.ts +3 -0
  378. package/src/tracing/index.ts +4 -0
  379. package/src/tracing/noop.ts +13 -0
  380. package/src/tracing/otel.ts +37 -0
  381. package/src/tracing/timing.ts +34 -0
  382. package/src/tracing/types.ts +9 -0
  383. package/src/url/index.ts +140 -0
  384. package/src/use-router.ts +11 -0
  385. package/src/use-suspense-query.ts +2 -0
  386. package/src/validation/index.ts +57 -0
  387. package/src/view-transition-css.ts +1 -0
@@ -0,0 +1,1589 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join, posix } from "node:path";
4
+ import { parse as babelParse } from "@babel/parser";
5
+ import { parseSeconds } from "../duration/index.ts";
6
+ import type { AuthenticateMode } from "../route-builder/types.ts";
7
+ import { extractLayoutKey } from "../router-primitives/paths.ts";
8
+
9
+ export type PrefetchStrategy = false | "intent" | "render" | "viewport";
10
+
11
+ export type ExtractedStaticDeferMode = "resolve" | "stream";
12
+
13
+ export interface ExtractedCacheConfig {
14
+ client?: {
15
+ cacheDeferred?: boolean;
16
+ gcTime?: number;
17
+ prefetch?: PrefetchStrategy;
18
+ prefetchGcTime?: number;
19
+ prefetchStaleTime?: number;
20
+ staleTime?: number;
21
+ };
22
+ isr?: true | "dynamic";
23
+ isrDefer?: ExtractedStaticDeferMode;
24
+ isrDynamicParams?: boolean;
25
+ isrRevalidate?: number;
26
+ ssg?: true | "dynamic";
27
+ ssgDefer?: ExtractedStaticDeferMode;
28
+ }
29
+
30
+ export interface ExtractedInterceptConfig {
31
+ from: string[];
32
+ render: string;
33
+ }
34
+
35
+ export interface RouteDefinition {
36
+ authenticateMode: AuthenticateMode;
37
+ exportName: string;
38
+ filePath: string;
39
+ cache: ExtractedCacheConfig;
40
+ hasInput: boolean;
41
+ intercept?: ExtractedInterceptConfig;
42
+ responseRoute: boolean;
43
+ type: "layout" | "page" | "path-segment" | "root-layout";
44
+ virtualPath: string;
45
+ }
46
+
47
+ export interface GenerateResult {
48
+ layouts: number;
49
+ routes: number;
50
+ warnings: string[];
51
+ }
52
+
53
+ /**
54
+ * Write a generated file with a checksum header.
55
+ * Skips the write when the existing file's checksum matches (idempotent).
56
+ */
57
+ function writeGenFile(filePath: string, body: string, generator: string): void {
58
+ const normalized = body.replace(/\n*$/, "\n");
59
+ const hash = createHash("sha256").update(normalized).digest("hex").slice(0, 12);
60
+
61
+ const ext = filePath.endsWith(".d.ts") ? ".d.ts" : extname(filePath);
62
+ const header =
63
+ ext === ".sql"
64
+ ? `-- @generated by ${generator} — do not edit. checksum: ${hash}`
65
+ : `/* @generated by ${generator} — do not edit. checksum: ${hash} */`;
66
+ const fullContent = `${header}\n${normalized}`;
67
+
68
+ if (existsSync(filePath)) {
69
+ const existing = readFileSync(filePath, "utf-8");
70
+ const firstLine = existing.split("\n")[0] ?? "";
71
+ const match = firstLine.match(/checksum: ([0-9a-f]{12})/);
72
+ if (match?.[1] === hash) return;
73
+ }
74
+
75
+ mkdirSync(dirname(filePath), { recursive: true });
76
+ writeFileSync(filePath, fullContent, "utf-8");
77
+ }
78
+
79
+ const BUILDER_TO_TYPE: Record<string, RouteDefinition["type"]> = {
80
+ createLayout: "layout",
81
+ createPage: "page",
82
+ createPathSegment: "path-segment",
83
+ createRootLayout: "root-layout",
84
+ };
85
+
86
+ /* Next export boundary — used to scope chain detection to the current definition */
87
+ const NEXT_EXPORT_RE = /\nexport\s+const\s+/;
88
+
89
+ const NUMERIC_RE = (key: string) => new RegExp(`${key}\\s*:\\s*([\\d_]+)`);
90
+ const DURATION_STR_RE = (key: string) => new RegExp(`${key}\\s*:\\s*["'\`](\\d+[smhd])["'\`]`);
91
+ const PREFETCH_RE = /prefetch\s*:\s*["'`](intent|render|viewport)["'`]/;
92
+
93
+ /** Escape a string for safe interpolation into generated double-quoted JS/TS string literals. */
94
+ function escapeCodegenStr(s: string): string {
95
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
96
+ }
97
+
98
+ /**
99
+ * Skip past a string/template/comment at position i, returning the index of the last char consumed.
100
+ * Returns i unchanged if the character at i is not a string/comment opener.
101
+ */
102
+ export function skipStringOrComment(code: string, start: number): number {
103
+ const ch = code[start];
104
+ let pos = start;
105
+
106
+ /* Single/double quoted strings */
107
+ if (ch === '"' || ch === "'") {
108
+ const q = ch;
109
+ pos++;
110
+ while (pos < code.length && code[pos] !== q) {
111
+ if (code[pos] === "\\") pos++;
112
+ pos++;
113
+ }
114
+ return pos;
115
+ }
116
+
117
+ /* Template literals */
118
+ if (ch === "`") {
119
+ pos++;
120
+ while (pos < code.length && code[pos] !== "`") {
121
+ if (code[pos] === "\\") {
122
+ pos++;
123
+ } else if (code[pos] === "$" && pos + 1 < code.length && code[pos + 1] === "{") {
124
+ pos += 2;
125
+ let tDepth = 1;
126
+ while (pos < code.length && tDepth > 0) {
127
+ if (code[pos] === "{") tDepth++;
128
+ else if (code[pos] === "}") tDepth--;
129
+ if (tDepth > 0) pos++;
130
+ }
131
+ }
132
+ pos++;
133
+ }
134
+ return pos;
135
+ }
136
+
137
+ /* Block comments */
138
+ if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "*") {
139
+ pos += 2;
140
+ while (pos < code.length && !(code[pos] === "*" && pos + 1 < code.length && code[pos + 1] === "/")) {
141
+ pos++;
142
+ }
143
+ return pos + 1;
144
+ }
145
+
146
+ /* Line comments */
147
+ if (ch === "/" && pos + 1 < code.length && code[pos + 1] === "/") {
148
+ pos += 2;
149
+ while (pos < code.length && code[pos] !== "\n") pos++;
150
+ return pos;
151
+ }
152
+
153
+ return start;
154
+ }
155
+
156
+ /**
157
+ * Extract the full argument of `.cache(...)` by counting braces.
158
+ * Returns the text between the outermost `{` and `}`, or null if not found.
159
+ * String-aware: skips braces inside strings, template literals, and comments.
160
+ */
161
+ function extractCacheArg(chainText: string): string | null {
162
+ const start = chainText.indexOf(".cache(");
163
+ if (start === -1) return null;
164
+
165
+ let depth = 0;
166
+ let openIdx = -1;
167
+ for (let i = start + 7; i < chainText.length; i++) {
168
+ const skipped = skipStringOrComment(chainText, i);
169
+ if (skipped !== i) {
170
+ i = skipped;
171
+ continue;
172
+ }
173
+ if (chainText[i] === "{") {
174
+ if (depth === 0) openIdx = i + 1;
175
+ depth++;
176
+ } else if (chainText[i] === "}") {
177
+ depth--;
178
+ if (depth === 0) return chainText.slice(openIdx, i);
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Extract the content of a nested `key: { ... }` block from a config body.
186
+ */
187
+ function extractNestedBlock(body: string, key: string): string | null {
188
+ const re = new RegExp(`${key}\\s*:\\s*\\{`);
189
+ const match = re.exec(body);
190
+ if (!match) return null;
191
+
192
+ let depth = 0;
193
+ const start = (match.index ?? 0) + match[0].length;
194
+ for (let i = start; i < body.length; i++) {
195
+ const skipped = skipStringOrComment(body, i);
196
+ if (skipped !== i) {
197
+ i = skipped;
198
+ continue;
199
+ }
200
+ if (body[i] === "{") depth++;
201
+ else if (body[i] === "}") {
202
+ if (depth === 0) return body.slice(start, i);
203
+ depth--;
204
+ }
205
+ }
206
+ return null;
207
+ }
208
+
209
+ function extractNumeric(text: string, key: string, unit?: "ms" | "s"): number | undefined {
210
+ /* try duration string first: key: "5m" */
211
+ const dm = DURATION_STR_RE(key).exec(text);
212
+ if (dm?.[1]) {
213
+ const seconds = parseSeconds(dm[1] as `${number}${"d" | "h" | "m" | "s"}`);
214
+ return unit === "ms" ? seconds * 1000 : seconds;
215
+ }
216
+ /* fall back to raw numeric: key: 5000 */
217
+ const m = NUMERIC_RE(key).exec(text);
218
+ if (!m) return undefined;
219
+ return Number((m[1] ?? "").replace(/_/g, ""));
220
+ }
221
+
222
+ export function extractCacheFromChain(chainText: string): ExtractedCacheConfig {
223
+ const config: ExtractedCacheConfig = {};
224
+
225
+ const body = extractCacheArg(chainText);
226
+ if (!body) return config;
227
+
228
+ /* client: { cacheDeferred, staleTime, gcTime, prefetch, prefetchStaleTime, prefetchGcTime } */
229
+ const clientBlock = extractNestedBlock(body, "client");
230
+ if (clientBlock) {
231
+ const client: NonNullable<ExtractedCacheConfig["client"]> = {};
232
+
233
+ if (/cacheDeferred\s*:\s*true/.test(clientBlock)) {
234
+ client.cacheDeferred = true;
235
+ }
236
+
237
+ const st = extractNumeric(clientBlock, "staleTime", "ms");
238
+ if (st !== undefined) client.staleTime = st;
239
+ const gc = extractNumeric(clientBlock, "gcTime", "ms");
240
+ if (gc !== undefined) client.gcTime = gc;
241
+ const pst = extractNumeric(clientBlock, "prefetchStaleTime", "ms");
242
+ if (pst !== undefined) client.prefetchStaleTime = pst;
243
+ const pgc = extractNumeric(clientBlock, "prefetchGcTime", "ms");
244
+ if (pgc !== undefined) client.prefetchGcTime = pgc;
245
+
246
+ const prefetchMatch = PREFETCH_RE.exec(clientBlock);
247
+ if (prefetchMatch) {
248
+ client.prefetch = prefetchMatch[1] as "intent" | "render" | "viewport";
249
+ }
250
+ if (/prefetch\s*:\s*false/.test(clientBlock)) {
251
+ client.prefetch = false;
252
+ }
253
+
254
+ if (Object.keys(client).length > 0) config.client = client;
255
+ }
256
+
257
+ /* ssg: true | () => ... | { defer?, params? } */
258
+ const ssgBlock = extractNestedBlock(body, "ssg");
259
+ if (ssgBlock) {
260
+ const hasParams = /params\s*:\s*(async\s+)?\(/.test(ssgBlock) || /params\s*:\s*(async\s+)?function/.test(ssgBlock);
261
+ config.ssg = hasParams ? "dynamic" : true;
262
+
263
+ const deferMatch = /defer\s*:\s*["'`](resolve|stream)["'`]/.exec(ssgBlock);
264
+ if (deferMatch) {
265
+ config.ssgDefer = deferMatch[1] as ExtractedStaticDeferMode;
266
+ }
267
+ } else if (/ssg\s*:\s*true/.test(body)) {
268
+ config.ssg = true;
269
+ } else if (/ssg\s*:\s*(async\s+)?\(/.test(body) || /ssg\s*:\s*(async\s+)?function/.test(body)) {
270
+ config.ssg = "dynamic";
271
+ }
272
+
273
+ /* isr: true | { revalidate?, params?, defer?, dynamicParams? } */
274
+ const isrBlock = extractNestedBlock(body, "isr");
275
+ if (isrBlock) {
276
+ const hasParams = /params\s*:\s*(async\s+)?\(/.test(isrBlock) || /params\s*:\s*(async\s+)?function/.test(isrBlock);
277
+ config.isr = hasParams ? "dynamic" : true;
278
+
279
+ const revalidate = extractNumeric(isrBlock, "revalidate", "s");
280
+ if (revalidate !== undefined) {
281
+ config.isrRevalidate = revalidate;
282
+ }
283
+
284
+ const deferMatch = /defer\s*:\s*["'`](resolve|stream)["'`]/.exec(isrBlock);
285
+ if (deferMatch) {
286
+ config.isrDefer = deferMatch[1] as ExtractedStaticDeferMode;
287
+ }
288
+
289
+ if (/dynamicParams\s*:\s*false/.test(isrBlock)) {
290
+ config.isrDynamicParams = false;
291
+ } else if (/dynamicParams\s*:\s*true/.test(isrBlock)) {
292
+ config.isrDynamicParams = true;
293
+ }
294
+ } else if (/isr\s*:\s*true/.test(body)) {
295
+ config.isr = true;
296
+ }
297
+
298
+ return config;
299
+ }
300
+
301
+ /**
302
+ * Extract the full argument of `.intercept(...)` by counting braces.
303
+ * Returns the text between the outermost `{` and `}`, or null if not found.
304
+ */
305
+ function extractInterceptArg(chainText: string): string | null {
306
+ const start = chainText.indexOf(".intercept(");
307
+ if (start === -1) return null;
308
+
309
+ let depth = 0;
310
+ let openIdx = -1;
311
+ for (let i = start + 11; i < chainText.length; i++) {
312
+ const skipped = skipStringOrComment(chainText, i);
313
+ if (skipped !== i) {
314
+ i = skipped;
315
+ continue;
316
+ }
317
+ if (chainText[i] === "{") {
318
+ if (depth === 0) openIdx = i + 1;
319
+ depth++;
320
+ } else if (chainText[i] === "}") {
321
+ depth--;
322
+ if (depth === 0) return chainText.slice(openIdx, i);
323
+ }
324
+ }
325
+ return null;
326
+ }
327
+
328
+ export function extractInterceptFromChain(chainText: string): ExtractedInterceptConfig | undefined {
329
+ const body = extractInterceptArg(chainText);
330
+ if (!body) return undefined;
331
+
332
+ /* Extract from: [...] array */
333
+ const fromMatch = /from\s*:\s*\[([^\]]*)\]/.exec(body);
334
+ if (!fromMatch) return undefined;
335
+ const fromRaw = fromMatch[1] ?? "";
336
+ const from = fromRaw
337
+ .split(",")
338
+ .map((s) => s.trim().replace(/^["'`]|["'`]$/g, ""))
339
+ .filter((s) => s.length > 0);
340
+
341
+ /* Extract render: "..." string */
342
+ const renderMatch = /render\s*:\s*["'`]([^"'`]+)["'`]/.exec(body);
343
+ if (!renderMatch) return undefined;
344
+ const render = renderMatch[1] ?? "";
345
+
346
+ if (from.length === 0 || render.length === 0) return undefined;
347
+
348
+ return { from, render };
349
+ }
350
+
351
+ /**
352
+ * Unwrap a member expression chain to find the root callee name.
353
+ * e.g. `createPage("x").cache({}).authenticate()` → the root call is `createPage`.
354
+ */
355
+ function findRootCallee(node: unknown): string | null {
356
+ const n = node as Record<string, unknown>;
357
+ if (n.type === "Identifier") return n.name as string;
358
+ if (n.type === "CallExpression") return findRootCallee(n.callee);
359
+ if (n.type === "MemberExpression") return findRootCallee(n.object);
360
+ return null;
361
+ }
362
+
363
+ /**
364
+ * Extract the first string argument from a CallExpression node's arguments.
365
+ * Handles the innermost call in a chain by recursing into callee.
366
+ */
367
+ function findInnermostCallArgs(node: unknown): unknown[] | null {
368
+ const n = node as Record<string, unknown>;
369
+ if (n.type !== "CallExpression") return null;
370
+
371
+ const callee = n.callee as Record<string, unknown>;
372
+ if (callee.type === "Identifier") return n.arguments as unknown[];
373
+ if (callee.type === "MemberExpression") {
374
+ const obj = callee.object as Record<string, unknown>;
375
+ return findInnermostCallArgs(obj);
376
+ }
377
+ if (callee.type === "CallExpression") return findInnermostCallArgs(callee);
378
+ return null;
379
+ }
380
+
381
+ function extractStringValue(node: unknown): string | null {
382
+ const n = node as Record<string, unknown>;
383
+ if (n.type === "StringLiteral") return n.value as string;
384
+ if (n.type === "TemplateLiteral") {
385
+ const quasis = n.quasis as Array<Record<string, unknown>>;
386
+ if (quasis.length === 1) {
387
+ const cooked = (quasis[0] as Record<string, unknown>).value as Record<string, unknown>;
388
+ return cooked.cooked as string;
389
+ }
390
+ }
391
+ return null;
392
+ }
393
+
394
+ export function extractRouteDefinitions(src: string, filePath: string): RouteDefinition[] {
395
+ let ast: ReturnType<typeof babelParse>;
396
+ try {
397
+ ast = babelParse(src, {
398
+ plugins: ["typescript", "jsx"],
399
+ sourceType: "module",
400
+ });
401
+ } catch {
402
+ return [];
403
+ }
404
+
405
+ const results: RouteDefinition[] = [];
406
+
407
+ for (const stmt of ast.program.body) {
408
+ if (stmt.type !== "ExportNamedDeclaration") continue;
409
+ const decl = stmt.declaration;
410
+ if (!decl || decl.type !== "VariableDeclaration") continue;
411
+
412
+ for (const declarator of decl.declarations) {
413
+ if (declarator.id.type !== "Identifier") continue;
414
+ if (!declarator.init) continue;
415
+
416
+ const rootCallee = findRootCallee(declarator.init);
417
+ if (!rootCallee) continue;
418
+
419
+ const type = BUILDER_TO_TYPE[rootCallee];
420
+ if (!type) continue;
421
+
422
+ const args = findInnermostCallArgs(declarator.init);
423
+ if (!args || args.length === 0) continue;
424
+
425
+ const virtualPath = extractStringValue(args[0]);
426
+ if (!virtualPath) continue;
427
+
428
+ const exportName = declarator.id.name;
429
+
430
+ /* Scope chain to text between this declaration's init start and next export boundary */
431
+ const initStart = declarator.init.start ?? 0;
432
+ const rest = src.slice(initStart);
433
+ const nextExport = NEXT_EXPORT_RE.exec(rest);
434
+ const chainText = nextExport ? rest.slice(0, nextExport.index) : rest;
435
+
436
+ const hasAuthenticateOptional = /\.authenticateOptional\s*\(/.test(chainText);
437
+ const hasAuthenticate = /\.authenticate\s*\(/.test(chainText);
438
+ const authenticateMode: AuthenticateMode = hasAuthenticateOptional ? "optional" : !!hasAuthenticate;
439
+ const hasInput = /\.input\s*(?:<[^>]*>)?\s*\(/.test(chainText);
440
+ const responseRoute = /\.response\s*\(/.test(chainText);
441
+ const cache = extractCacheFromChain(chainText);
442
+ const intercept = extractInterceptFromChain(chainText);
443
+
444
+ results.push({
445
+ authenticateMode,
446
+ cache,
447
+ exportName,
448
+ filePath,
449
+ hasInput,
450
+ intercept,
451
+ responseRoute,
452
+ type,
453
+ virtualPath,
454
+ });
455
+ }
456
+ }
457
+
458
+ return results;
459
+ }
460
+
461
+ export function validateRouteDefinitions(defs: RouteDefinition[]): string[] {
462
+ const errors: string[] = [];
463
+ const pages = new Map<string, string>();
464
+ const layouts = new Map<string, string>();
465
+ const rootLayouts = new Map<string, string>();
466
+
467
+ /* 1. Virtual path uniqueness per type */
468
+ for (const def of defs) {
469
+ let bucket = pages;
470
+ if (def.type === "root-layout") bucket = rootLayouts;
471
+ else if (def.type === "layout") bucket = layouts;
472
+ const existing = bucket.get(def.virtualPath);
473
+ if (existing) {
474
+ errors.push(`Duplicate ${def.type} at "${def.virtualPath}" in ${def.filePath} (already defined in ${existing})`);
475
+ } else {
476
+ bucket.set(def.virtualPath, def.filePath);
477
+ }
478
+ }
479
+
480
+ /* 2. Variable path conflicts (different virtual paths → same resolved URL) */
481
+ const pagesByVarPath = new Map<string, { filePath: string; virtualPath: string }>();
482
+ for (const def of defs) {
483
+ if (def.type !== "page") continue;
484
+ const varPath = computeUrlPattern(def.virtualPath);
485
+ const existing = pagesByVarPath.get(varPath);
486
+ if (existing && existing.virtualPath !== def.virtualPath) {
487
+ errors.push(
488
+ `Variable path conflict: "${def.virtualPath}" and "${existing.virtualPath}" both resolve to "${varPath}" (${def.filePath} vs ${existing.filePath})`,
489
+ );
490
+ } else if (!existing) {
491
+ pagesByVarPath.set(varPath, { filePath: def.filePath, virtualPath: def.virtualPath });
492
+ }
493
+ }
494
+
495
+ /* 3. Pre-root segments must be param-only */
496
+ for (const def of defs) {
497
+ const parts = def.virtualPath.split("/");
498
+ const rootIdx = parts.findIndex((p) => p.startsWith("_") && p.endsWith("_") && p.length >= 3);
499
+ if (rootIdx > 0) {
500
+ for (let i = 0; i < rootIdx; i++) {
501
+ const seg = parts[i] ?? "";
502
+ if (!seg.startsWith("[")) {
503
+ errors.push(
504
+ `Invalid pre-root segment "${seg}" in "${def.virtualPath}" (${def.filePath}). Only [param], [[param]], [...param], [[...param]] segments allowed before root layout.`,
505
+ );
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ /* 4. Catch-all conflicts: a catch-all page shadows a more specific sibling page */
512
+ const pageDefs = defs.filter((d) => d.type === "page");
513
+ for (const catchAll of pageDefs) {
514
+ const parts = catchAll.virtualPath.split("/");
515
+ const lastPart = parts[parts.length - 1] ?? "";
516
+ if (!lastPart.startsWith("[...") && !lastPart.startsWith("[[...")) continue;
517
+
518
+ const prefix = parts.slice(0, -1).join("/");
519
+ for (const other of pageDefs) {
520
+ if (other === catchAll) continue;
521
+ if (other.virtualPath.startsWith(`${prefix}/`) && other.virtualPath !== catchAll.virtualPath) {
522
+ errors.push(
523
+ `Catch-all conflict: "${catchAll.virtualPath}" shadows "${other.virtualPath}" (${catchAll.filePath} vs ${other.filePath})`,
524
+ );
525
+ }
526
+ }
527
+ }
528
+
529
+ return errors;
530
+ }
531
+
532
+ function computeUrlPattern(virtualPath: string): string {
533
+ /* Remove root prefix (+ pre-root segments) and group segments */
534
+ const parts = virtualPath.split("/");
535
+ const rootIdx = parts.findIndex((p) => p.startsWith("_") && p.endsWith("_") && p.length >= 3);
536
+ const urlParts: string[] = [];
537
+
538
+ for (let i = 0; i < parts.length; i++) {
539
+ const part = parts[i] ?? "";
540
+ /* Skip the root layout segment itself (e.g. _root_) */
541
+ if (i === rootIdx) continue;
542
+ /* Skip non-param pre-root segments */
543
+ if (rootIdx >= 0 && i < rootIdx && !part.startsWith("[")) continue;
544
+ /* Skip group segments (e.g. (shop)) */
545
+ if (part.startsWith("(") && part.endsWith(")")) continue;
546
+ /* Skip empty segments (trailing slash = index route) */
547
+ if (part === "") continue;
548
+ urlParts.push(part);
549
+ }
550
+
551
+ const url = `/${urlParts.join("/")}`;
552
+ return url === "" ? "/" : url;
553
+ }
554
+
555
+ function relativeImportPath(from: string, to: string): string {
556
+ const fromParts = from.split("/");
557
+ const toParts = to.split("/");
558
+
559
+ /* Remove filename from 'from' */
560
+ fromParts.pop();
561
+
562
+ /* Find common prefix */
563
+ let common = 0;
564
+ while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
565
+ common++;
566
+ }
567
+
568
+ const ups = fromParts.length - common;
569
+ const downs = toParts.slice(common);
570
+
571
+ const prefix = ups > 0 ? Array(ups).fill("..").join("/") : ".";
572
+
573
+ /* Remove .ts/.tsx extension */
574
+ const last = downs[downs.length - 1] ?? "";
575
+ downs[downs.length - 1] = last.replace(/\.(tsx?|ts)$/, "");
576
+
577
+ return `${prefix}/${downs.join("/")}`;
578
+ }
579
+
580
+ function formatRouteMeta(def: RouteDefinition): string {
581
+ const parts: string[] = [];
582
+ if (def.authenticateMode === true) parts.push("authenticate: true");
583
+ else if (def.authenticateMode === "optional") parts.push(`authenticate: "optional"`);
584
+
585
+ if (def.cache.client) {
586
+ const cp: string[] = [];
587
+ if (def.cache.client.cacheDeferred === true) cp.push("cacheDeferred: true");
588
+ if (def.cache.client.staleTime !== undefined) cp.push(`staleTime: ${def.cache.client.staleTime}`);
589
+ if (def.cache.client.gcTime !== undefined) cp.push(`gcTime: ${def.cache.client.gcTime}`);
590
+ if (def.cache.client.prefetch !== undefined) {
591
+ const pv = def.cache.client.prefetch;
592
+ cp.push(pv === false ? "prefetch: false" : `prefetch: "${pv}"`);
593
+ }
594
+ if (def.cache.client.prefetchStaleTime !== undefined)
595
+ cp.push(`prefetchStaleTime: ${def.cache.client.prefetchStaleTime}`);
596
+ if (def.cache.client.prefetchGcTime !== undefined) cp.push(`prefetchGcTime: ${def.cache.client.prefetchGcTime}`);
597
+ if (cp.length > 0) parts.push(`client: { ${cp.join(", ")} }`);
598
+ }
599
+
600
+ if (def.cache.isr) {
601
+ const sp: string[] = [`mode: "isr"`];
602
+ if (def.cache.isrDefer) sp.push(`defer: "${def.cache.isrDefer}"`);
603
+ if (def.cache.isrDynamicParams === false) {
604
+ sp.push("dynamicParams: false");
605
+ }
606
+ if (def.cache.isrRevalidate !== undefined) {
607
+ sp.push(`revalidate: ${def.cache.isrRevalidate}`);
608
+ }
609
+ parts.push(`static: { ${sp.join(", ")} }`);
610
+ } else if (def.cache.ssg) {
611
+ const sp: string[] = [`mode: "static"`];
612
+ if (def.cache.ssgDefer) sp.push(`defer: "${def.cache.ssgDefer}"`);
613
+ parts.push(`static: { ${sp.join(", ")} }`);
614
+ }
615
+
616
+ if (def.intercept) {
617
+ const fromArr = def.intercept.from.map((f) => `"${escapeCodegenStr(f)}"`).join(", ");
618
+ parts.push(`intercept: { from: [${fromArr}], render: "${escapeCodegenStr(def.intercept.render)}" }`);
619
+ }
620
+
621
+ return parts.length > 0 ? ` ${parts.join(", ")} ` : "";
622
+ }
623
+
624
+ export function generateRouteInserts(defs: RouteDefinition[], outputDir: string): string {
625
+ const pages = defs.filter((d) => d.type === "page");
626
+ const lines: string[] = [];
627
+
628
+ for (const def of pages) {
629
+ const urlPattern = computeUrlPattern(def.virtualPath);
630
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
631
+ const routeType = def.responseRoute ? "x" : "r";
632
+ const meta = formatRouteMeta(def);
633
+
634
+ lines.push(
635
+ `insertRoute(tree, "${escapeCodegenStr(urlPattern)}", {`,
636
+ `\te: "${escapeCodegenStr(def.exportName)}",`,
637
+ `\to: {${meta}},`,
638
+ `\tp: () => import("${escapeCodegenStr(importPath)}").then(m => ({ default: m["${escapeCodegenStr(def.exportName)}"] })),`,
639
+ `\tt: "${routeType}",`,
640
+ `\tv: "${escapeCodegenStr(urlPattern)}",`,
641
+ `\tx: "${escapeCodegenStr(def.virtualPath)}",`,
642
+ "})",
643
+ "",
644
+ );
645
+ }
646
+
647
+ return lines.join("\n");
648
+ }
649
+
650
+ export function generateLayoutsRecord(defs: RouteDefinition[], outputDir: string): string {
651
+ const layouts = defs.filter((d) => d.type === "layout" || d.type === "root-layout" || d.type === "path-segment");
652
+
653
+ if (layouts.length === 0) {
654
+ return [
655
+ "export const layouts: Record<string, () => Promise<{ default: unknown }>> = {}",
656
+ "export const layoutModuleIds: Record<string, string> = {}",
657
+ ].join("\n");
658
+ }
659
+
660
+ const entries: string[] = [];
661
+ const moduleIdEntries: string[] = [];
662
+ for (const def of layouts) {
663
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
664
+ const key = def.type === "path-segment" ? extractLayoutKey(def.virtualPath) : def.virtualPath;
665
+ entries.push(
666
+ `\t"${escapeCodegenStr(key)}": () => import("${escapeCodegenStr(importPath)}").then(m => ({ default: m["${escapeCodegenStr(def.exportName)}"] })),`,
667
+ );
668
+ moduleIdEntries.push(`\t"${escapeCodegenStr(key)}": "${escapeCodegenStr(def.filePath)}",`);
669
+ }
670
+
671
+ return [
672
+ "export const layouts: Record<string, () => Promise<{ default: unknown }>> = {",
673
+ ...entries,
674
+ "}",
675
+ "",
676
+ "export const layoutModuleIds: Record<string, string> = {",
677
+ ...moduleIdEntries,
678
+ "}",
679
+ ].join("\n");
680
+ }
681
+
682
+ export interface TreeBuildNode {
683
+ c?: TreeBuildNode;
684
+ n?: string;
685
+ o?: TreeBuildNode;
686
+ p?: TreeBuildNode;
687
+ q?: TreeBuildNode;
688
+ routeKey?: string;
689
+ s: Record<string, TreeBuildNode>;
690
+ }
691
+
692
+ function createBuildNode(paramName?: string): TreeBuildNode {
693
+ const node: TreeBuildNode = { s: {} };
694
+ if (paramName !== undefined) node.n = paramName;
695
+ return node;
696
+ }
697
+
698
+ export function buildRouteTree(routes: Array<{ routeKey: string; urlPattern: string }>): TreeBuildNode {
699
+ const root = createBuildNode();
700
+
701
+ for (const { routeKey, urlPattern } of routes) {
702
+ const segments = urlPattern.split("/").filter((seg) => seg.length > 0);
703
+ let node = root;
704
+
705
+ for (const segment of segments) {
706
+ const optionalCatchAll = segment.match(/^\[\[\.\.\.(\w+)\]\]$/);
707
+ if (optionalCatchAll) {
708
+ if (!node.o) node.o = createBuildNode(optionalCatchAll[1]);
709
+ node = node.o;
710
+ continue;
711
+ }
712
+
713
+ const optionalParam = segment.match(/^\[\[(\w+)\]\]$/);
714
+ if (optionalParam) {
715
+ if (!node.q) node.q = createBuildNode(optionalParam[1]);
716
+ node = node.q;
717
+ continue;
718
+ }
719
+
720
+ const catchAll = segment.match(/^\[\.\.\.(\w+)\]$/);
721
+ if (catchAll) {
722
+ if (!node.c) node.c = createBuildNode(catchAll[1]);
723
+ node = node.c;
724
+ continue;
725
+ }
726
+
727
+ const param = segment.match(/^\[(\w+)\]$/);
728
+ if (param) {
729
+ if (!node.p) node.p = createBuildNode(param[1]);
730
+ node = node.p;
731
+ continue;
732
+ }
733
+
734
+ const key = segment.toLowerCase();
735
+ let child = node.s[key];
736
+ if (!child) {
737
+ child = createBuildNode();
738
+ node.s[key] = child;
739
+ }
740
+ node = child;
741
+ }
742
+
743
+ node.routeKey = routeKey;
744
+ }
745
+
746
+ return root;
747
+ }
748
+
749
+ const VALID_IDENT_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
750
+
751
+ function quoteKey(key: string): string {
752
+ return VALID_IDENT_RE.test(key) ? key : `"${key}"`;
753
+ }
754
+
755
+ export function serializeTreeNode(node: TreeBuildNode): string {
756
+ const parts: string[] = [];
757
+
758
+ if (node.c) parts.push(`c: ${serializeTreeNode(node.c)}`);
759
+ if (node.n !== undefined) parts.push(`n: "${node.n}"`);
760
+ if (node.o) parts.push(`o: ${serializeTreeNode(node.o)}`);
761
+ if (node.p) parts.push(`p: ${serializeTreeNode(node.p)}`);
762
+ if (node.q) parts.push(`q: ${serializeTreeNode(node.q)}`);
763
+ if (node.routeKey) parts.push(`r: ${node.routeKey}`);
764
+
765
+ const entries = Object.entries(node.s);
766
+ if (entries.length === 0) {
767
+ parts.push("s: E");
768
+ } else {
769
+ const serialized = entries.map(([k, child]) => `${quoteKey(k)}: ${serializeTreeNode(child)}`).join(", ");
770
+ parts.push(`s: S({ ${serialized} })`);
771
+ }
772
+
773
+ return `{ ${parts.join(", ")} }`;
774
+ }
775
+
776
+ function serializeRouteMeta(def: RouteDefinition): string {
777
+ const meta = formatRouteMeta(def);
778
+ return `{${meta}}`;
779
+ }
780
+
781
+ export function generateRoutesFile(defs: RouteDefinition[], outputDir: string): string {
782
+ const pages = defs.filter((d) => d.type === "page");
783
+ const layouts = generateLayoutsRecord(defs, outputDir);
784
+
785
+ /* Detect whether any auth routes exist (for auth type derivation) */
786
+ const hasAuth = defs.some(
787
+ (d) => !d.responseRoute && (d.authenticateMode === true || d.authenticateMode === "optional"),
788
+ );
789
+
790
+ /* Detect whether any pages have .input() for search params typing */
791
+ const hasSearchParams = pages.some((d) => !d.responseRoute && d.hasInput);
792
+
793
+ /* Detect queryClientGetter in router source */
794
+ const routerFilePath = posix.join(posix.dirname(outputDir), "router");
795
+ let hasQueryClient = false;
796
+ for (const ext of [".ts", ".tsx"]) {
797
+ try {
798
+ const routerSource = readFileSync(`${routerFilePath}${ext}`, "utf-8");
799
+ if (routerSource.includes("queryClientGetter")) {
800
+ hasQueryClient = true;
801
+ }
802
+ break;
803
+ } catch {
804
+ /* file doesn't exist with this extension */
805
+ }
806
+ }
807
+
808
+ const serverHandler = resolveCreateServerExport(outputDir);
809
+ const registry = generateRouteRegistry(defs, outputDir, hasAuth, hasQueryClient, serverHandler !== null);
810
+
811
+ /* Deduplicate meta objects */
812
+ const metaMap = new Map<string, string>();
813
+ const metaDecls: string[] = [];
814
+ let metaIdx = 0;
815
+
816
+ for (const def of pages) {
817
+ const metaStr = serializeRouteMeta(def);
818
+ if (!metaMap.has(metaStr)) {
819
+ const varName = `O${metaIdx++}`;
820
+ metaMap.set(metaStr, varName);
821
+ metaDecls.push(`const ${varName} = ${metaStr} as const`);
822
+ }
823
+ }
824
+
825
+ /* Build route declarations */
826
+ const routeDecls: string[] = [];
827
+ const treeInputs: Array<{ routeKey: string; urlPattern: string }> = [];
828
+
829
+ for (let i = 0; i < pages.length; i++) {
830
+ const def = pages[i];
831
+ if (!def) continue;
832
+ const routeKey = `R${i}`;
833
+ const urlPattern = computeUrlPattern(def.virtualPath);
834
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
835
+ const routeType = def.responseRoute ? "x" : "r";
836
+ const metaVar = metaMap.get(serializeRouteMeta(def));
837
+
838
+ routeDecls.push(
839
+ [
840
+ `const ${routeKey}: RouteData = {`,
841
+ `\te: "${def.exportName}",`,
842
+ `\tf: "${def.filePath}",`,
843
+ `\to: ${metaVar},`,
844
+ `\tp: () => import("${importPath}").then(m => ({ default: m.${def.exportName} })),`,
845
+ `\tt: "${routeType}",`,
846
+ `\tv: "${urlPattern}",`,
847
+ `\tx: "${def.virtualPath}",`,
848
+ "}",
849
+ ].join("\n"),
850
+ );
851
+
852
+ treeInputs.push({ routeKey, urlPattern });
853
+ }
854
+
855
+ /* Build and serialize tree */
856
+ const tree = buildRouteTree(treeInputs);
857
+ const treeStr = serializeTreeNode(tree);
858
+
859
+ const flareImports: string[] = ["TreeNode"];
860
+ if (pages.length > 0) flareImports.push("RouteData");
861
+ if (hasSearchParams) flareImports.push("InferParams", "InferSearchParams");
862
+ flareImports.sort();
863
+
864
+ const lines: string[] = [
865
+ "/* Auto-generated by flare — do not edit */",
866
+ `import type { ${flareImports.join(", ")} } from "@lovrozagar/flare/codegen"`,
867
+ ];
868
+ if (serverHandler) {
869
+ lines.push(`import type { ${serverHandler.exportName} as _FlareHandler } from "${serverHandler.importPath}"`);
870
+ }
871
+ lines.push("", "const E: Record<string, TreeNode> = Object.create(null)");
872
+
873
+ if (treeStr.includes("S(")) {
874
+ lines.push(
875
+ "function S(o: Record<string, TreeNode>): Record<string, TreeNode> { return Object.assign(Object.create(null), o) }",
876
+ );
877
+ }
878
+
879
+ if (metaDecls.length > 0) {
880
+ lines.push("", ...metaDecls);
881
+ }
882
+
883
+ if (routeDecls.length > 0) {
884
+ lines.push("", ...routeDecls);
885
+ }
886
+
887
+ lines.push("", `export const routeTree: TreeNode = ${treeStr}`, "", layouts, registry);
888
+
889
+ return lines.join("\n");
890
+ }
891
+
892
+ export interface RouteParam {
893
+ name: string;
894
+ type: string;
895
+ }
896
+
897
+ /** Extract param names and types from a URL pattern. */
898
+ export function extractParamsFromPattern(urlPattern: string): RouteParam[] {
899
+ const params: RouteParam[] = [];
900
+ const parts = urlPattern.split("/");
901
+
902
+ for (const part of parts) {
903
+ /* Optional catch-all: [[...name]] */
904
+ const optCatch = part.match(/^\[\[\.\.\.(.+?)\]\]$/);
905
+ if (optCatch) {
906
+ params.push({ name: optCatch[1] ?? "", type: "string[] | undefined" });
907
+ continue;
908
+ }
909
+ /* Required catch-all: [...name] */
910
+ const reqCatch = part.match(/^\[\.\.\.(.+?)\]$/);
911
+ if (reqCatch) {
912
+ params.push({ name: reqCatch[1] ?? "", type: "string[]" });
913
+ continue;
914
+ }
915
+ /* Optional single param: [[name]] */
916
+ const optSingle = part.match(/^\[\[(\w+)\]\]$/);
917
+ if (optSingle) {
918
+ params.push({ name: optSingle[1] ?? "", type: "string | undefined" });
919
+ continue;
920
+ }
921
+ /* Required single param: [name] */
922
+ const single = part.match(/^\[(.+?)\]$/);
923
+ if (single) {
924
+ params.push({ name: single[1] ?? "", type: "string" });
925
+ }
926
+ }
927
+
928
+ return params.sort((a, b) => a.name.localeCompare(b.name));
929
+ }
930
+
931
+ /** Compute parent layout virtualPaths for a given route, sorted root-first. */
932
+ export function computeParentLayouts(virtualPath: string, layoutPaths: string[]): string[] {
933
+ return layoutPaths
934
+ .filter((lp) => lp !== virtualPath && virtualPath.startsWith(`${lp}/`))
935
+ .sort((a, b) => a.length - b.length);
936
+ }
937
+
938
+ /** Generate virtual module type declarations as a reference to the flare package types. */
939
+ export function generateVirtualModuleTypes(): string {
940
+ return `/// <reference types="vite/client" />\n/// <reference types="@lovrozagar/flare/virtual-types" />\n`;
941
+ }
942
+
943
+ function resolveCreateServerExport(outputDir: string): { exportName: string; importPath: string } | null {
944
+ const serverFilePath = posix.join(posix.dirname(outputDir), "server");
945
+ for (const ext of [".ts", ".tsx"]) {
946
+ try {
947
+ const serverSource = readFileSync(`${serverFilePath}${ext}`, "utf-8");
948
+ const match = serverSource.match(/export\s+const\s+(\w+)\s*=\s*createServer/);
949
+ if (match?.[1]) {
950
+ return {
951
+ exportName: match[1],
952
+ importPath: relativeImportPath(`${outputDir}/routes.gen.ts`, serverFilePath),
953
+ };
954
+ }
955
+ } catch {
956
+ /* missing extension */
957
+ }
958
+ }
959
+ return null;
960
+ }
961
+
962
+ /** Generate TypeScript declaration merging block for route type registry. */
963
+ export function generateRouteRegistry(
964
+ defs: RouteDefinition[],
965
+ outputDir: string,
966
+ hasAuth?: boolean,
967
+ hasQueryClient?: boolean,
968
+ hasServer = false,
969
+ ): string {
970
+ /* routes: URL path → params (pages only, no response routes) */
971
+ const pages = defs
972
+ .filter((d) => d.type === "page" && !d.responseRoute)
973
+ .map((d) => ({
974
+ params: extractParamsFromPattern(computeUrlPattern(d.virtualPath)),
975
+ url: computeUrlPattern(d.virtualPath),
976
+ }))
977
+ .sort((a, b) => a.url.localeCompare(b.url));
978
+
979
+ const routeEntries: string[] = [];
980
+ for (const page of pages) {
981
+ if (page.params.length === 0) {
982
+ routeEntries.push(`\t\t\t"${escapeCodegenStr(page.url)}": {}`);
983
+ } else {
984
+ const paramEntries = page.params.map((p) => `${p.name}: ${p.type}`).join(", ");
985
+ routeEntries.push(`\t\t\t"${escapeCodegenStr(page.url)}": { params: { ${paramEntries} } }`);
986
+ }
987
+ }
988
+
989
+ /* routeModules: virtualPath → typeof import(...)["exportName"] (all non-response) */
990
+ const moduleDefs = defs.filter((d) => !d.responseRoute).sort((a, b) => a.virtualPath.localeCompare(b.virtualPath));
991
+
992
+ const moduleEntries: string[] = [];
993
+ for (const def of moduleDefs) {
994
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
995
+ moduleEntries.push(
996
+ `\t\t\t"${escapeCodegenStr(def.virtualPath)}": typeof import("${escapeCodegenStr(importPath)}")["${escapeCodegenStr(def.exportName)}"]`,
997
+ );
998
+ }
999
+
1000
+ /* routeParents: virtualPath → parent layout chain tuple */
1001
+ const layoutPaths = defs.filter((d) => d.type === "layout" || d.type === "root-layout").map((d) => d.virtualPath);
1002
+
1003
+ const parentEntries: string[] = [];
1004
+ for (const def of moduleDefs) {
1005
+ const parents = computeParentLayouts(def.virtualPath, layoutPaths);
1006
+ if (parents.length === 0) {
1007
+ parentEntries.push(`\t\t\t"${escapeCodegenStr(def.virtualPath)}": []`);
1008
+ } else {
1009
+ const tuple = parents.map((p) => `"${escapeCodegenStr(p)}"`).join(", ");
1010
+ parentEntries.push(`\t\t\t"${escapeCodegenStr(def.virtualPath)}": [${tuple}]`);
1011
+ }
1012
+ }
1013
+
1014
+ /* authModes: virtualPath → AuthenticateMode (only for non-false entries) */
1015
+ const authEntries: string[] = [];
1016
+ for (const def of moduleDefs) {
1017
+ if (def.authenticateMode === true) {
1018
+ authEntries.push(`\t\t\t"${escapeCodegenStr(def.virtualPath)}": true`);
1019
+ } else if (def.authenticateMode === "optional") {
1020
+ authEntries.push(`\t\t\t"${escapeCodegenStr(def.virtualPath)}": "optional"`);
1021
+ }
1022
+ }
1023
+
1024
+ const lines = [
1025
+ "",
1026
+ "/* Route type registry — auto-generated */",
1027
+ 'declare module "@lovrozagar/flare/codegen" {',
1028
+ "\tinterface FlareRegister {",
1029
+ "\t\troutes: {",
1030
+ ...routeEntries,
1031
+ "\t\t}",
1032
+ "\t\trouteModules: {",
1033
+ ...moduleEntries,
1034
+ "\t\t}",
1035
+ "\t\trouteParents: {",
1036
+ ...parentEntries,
1037
+ "\t\t}",
1038
+ ];
1039
+
1040
+ if (authEntries.length > 0) {
1041
+ lines.push("\t\tauthModes: {");
1042
+ lines.push(...authEntries);
1043
+ lines.push("\t\t}");
1044
+ }
1045
+
1046
+ /* routeSearchParams: URL path → InferSearchParams for pages with .input() */
1047
+ const searchEntries: string[] = [];
1048
+ for (const page of pages) {
1049
+ const def = defs.find((d) => !d.responseRoute && d.hasInput && computeUrlPattern(d.virtualPath) === page.url);
1050
+ if (def) {
1051
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
1052
+ searchEntries.push(
1053
+ `\t\t\t"${escapeCodegenStr(page.url)}": InferSearchParams<typeof import("${escapeCodegenStr(importPath)}")["${escapeCodegenStr(def.exportName)}"]>`,
1054
+ );
1055
+ }
1056
+ }
1057
+
1058
+ if (searchEntries.length > 0) {
1059
+ lines.push("\t\trouteSearchParams: {");
1060
+ lines.push(...searchEntries);
1061
+ lines.push("\t\t}");
1062
+ }
1063
+
1064
+ /* routeParams: URL path → InferParams for pages with .input() */
1065
+ const paramInferEntries: string[] = [];
1066
+ for (const page of pages) {
1067
+ const def = defs.find((d) => !d.responseRoute && d.hasInput && computeUrlPattern(d.virtualPath) === page.url);
1068
+ if (def) {
1069
+ const importPath = relativeImportPath(`${outputDir}/routes.gen.ts`, def.filePath);
1070
+ paramInferEntries.push(
1071
+ `\t\t\t"${escapeCodegenStr(page.url)}": InferParams<typeof import("${escapeCodegenStr(importPath)}")["${escapeCodegenStr(def.exportName)}"]>`,
1072
+ );
1073
+ }
1074
+ }
1075
+
1076
+ if (paramInferEntries.length > 0) {
1077
+ lines.push("\t\trouteParams: {");
1078
+ lines.push(...paramInferEntries);
1079
+ lines.push("\t\t}");
1080
+ }
1081
+
1082
+ if (hasServer && hasAuth) {
1083
+ lines.push(`\t\tauth: NonNullable<Awaited<ReturnType<NonNullable<typeof _FlareHandler["authenticateFn"]>>>>`);
1084
+ }
1085
+
1086
+ if (hasServer) {
1087
+ lines.push(`\t\tenv: Parameters<typeof _FlareHandler["fetch"]>[1]`);
1088
+ lines.push(`\t\tserverContext: Awaited<ReturnType<NonNullable<typeof _FlareHandler["serverContext"]>>>`);
1089
+ }
1090
+ if (hasQueryClient) {
1091
+ lines.push("\t\thasQueryClient: true");
1092
+ }
1093
+
1094
+ lines.push("\t}", "}", "");
1095
+ return lines.join("\n");
1096
+ }
1097
+
1098
+ export interface ScanOptions {
1099
+ ignorePrefix?: string;
1100
+ rootDir: string;
1101
+ srcDir?: string;
1102
+ }
1103
+
1104
+ export function scanSourceFiles(options: ScanOptions): RouteDefinition[] {
1105
+ const srcPath = join(options.rootDir, options.srcDir ?? "src");
1106
+ const prefix = options.ignorePrefix ?? "_";
1107
+ const defs: RouteDefinition[] = [];
1108
+
1109
+ let entries: string[];
1110
+ try {
1111
+ entries = readdirSync(srcPath, { recursive: true }) as string[];
1112
+ } catch {
1113
+ return defs;
1114
+ }
1115
+
1116
+ for (const entry of entries) {
1117
+ /* Normalize to posix separators */
1118
+ const rel = entry.replace(/\\/g, "/");
1119
+
1120
+ /* Skip non-ts files */
1121
+ if (!rel.endsWith(".ts") && !rel.endsWith(".tsx")) continue;
1122
+
1123
+ /* Skip generated files and node_modules */
1124
+ if (rel.includes("_gen/") || rel.includes("node_modules")) continue;
1125
+ if (rel.endsWith(".gen.ts") || rel.endsWith(".gen.tsx")) continue;
1126
+
1127
+ /* Skip directories starting with ignorePrefix (except _word_ route convention like _root_) */
1128
+ const segments = rel.split("/");
1129
+ /* _word_ or __word__ patterns are route conventions, not ignored dirs */
1130
+ const isRouteConvention = (seg: string) => /^_[^_].*[^_]_$|^__[^_].*[^_]__$/.test(seg);
1131
+ const hasIgnoredDir = segments.some(
1132
+ (seg, i) => i < segments.length - 1 && seg.startsWith(prefix) && !isRouteConvention(seg),
1133
+ );
1134
+ if (hasIgnoredDir) continue;
1135
+
1136
+ const fullPath = posix.join(srcPath, rel);
1137
+ let content: string;
1138
+ try {
1139
+ content = readFileSync(fullPath, "utf-8");
1140
+ } catch {
1141
+ continue;
1142
+ }
1143
+
1144
+ defs.push(...extractRouteDefinitions(content, posix.join(options.srcDir ?? "src", rel)));
1145
+ }
1146
+
1147
+ return defs;
1148
+ }
1149
+
1150
+ /* ── Filesystem-based codegen ─────────────────────────────────────── */
1151
+
1152
+ const ROUTE_SUFFIX_RE = /\.(page|layout|root-layout|path-segment)\.(tsx?|jsx?)$/;
1153
+
1154
+ const BUILDER_EXPORT_RE =
1155
+ /export\s+const\s+\w+\s*=\s*(createPage|createLayout|createRootLayout|createPathSegment)\s*(?:<[^>]*>)?\s*\(/;
1156
+
1157
+ const ROOT_SCOPE_RE = /^_[^/]+_$/;
1158
+
1159
+ export type FsLayoutIssueKind = "missing-root-scope" | "string-style-route" | "suffix-outside-routes";
1160
+
1161
+ export interface FsLayoutIssue {
1162
+ filePath: string;
1163
+ kind: FsLayoutIssueKind;
1164
+ line?: number;
1165
+ message: string;
1166
+ }
1167
+
1168
+ export function formatFsLayoutWarning(issue: FsLayoutIssue): string {
1169
+ const loc = issue.line !== undefined ? `${issue.filePath}:${issue.line}` : issue.filePath;
1170
+ return `${loc} — ${issue.message}`;
1171
+ }
1172
+
1173
+ /** Detect route type from filename suffix. Returns null for non-route files. */
1174
+ export function detectRouteType(fileName: string): RouteDefinition["type"] | null {
1175
+ const match = ROUTE_SUFFIX_RE.exec(fileName);
1176
+ if (!match) return null;
1177
+ return (match[1] ?? null) as RouteDefinition["type"] | null;
1178
+ }
1179
+
1180
+ const BUILDER_MAP: Record<RouteDefinition["type"], string> = {
1181
+ layout: "createLayout",
1182
+ page: "createPage",
1183
+ "path-segment": "createPathSegment",
1184
+ "root-layout": "createRootLayout",
1185
+ };
1186
+
1187
+ const BUILDER_SUBPATH_MAP: Record<string, string> = {
1188
+ createLayout: "@lovrozagar/flare/layout",
1189
+ createPage: "@lovrozagar/flare/page",
1190
+ createPathSegment: "@lovrozagar/flare/path-segment",
1191
+ createRootLayout: "@lovrozagar/flare/root-layout",
1192
+ };
1193
+
1194
+ function builderSubpath(fn: string): string {
1195
+ return BUILDER_SUBPATH_MAP[fn] ?? `@lovrozagar/flare/${fn.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
1196
+ }
1197
+
1198
+ /**
1199
+ * Derive a virtual path from a file's position in the routes/ directory.
1200
+ * Folder structure maps directly to virtual path. Returns null for ignored dirs.
1201
+ */
1202
+ export function deriveVirtualPath(filePath: string, routesDir: string): string | null {
1203
+ const withoutRoutes = filePath.startsWith(`${routesDir}/`) ? filePath.slice(routesDir.length + 1) : filePath;
1204
+ const segments = withoutRoutes.split("/");
1205
+
1206
+ /* Remove filename */
1207
+ segments.pop();
1208
+
1209
+ const pathParts: string[] = [];
1210
+
1211
+ for (const seg of segments) {
1212
+ /* [_]name → escaped underscore, literal _name segment */
1213
+ if (seg.startsWith("[_]")) {
1214
+ pathParts.push(`_${seg.slice(3)}`);
1215
+ continue;
1216
+ }
1217
+
1218
+ /* _name (single underscore, no trailing) → ignored directory */
1219
+ if (seg.startsWith("_") && !seg.endsWith("_")) {
1220
+ return null;
1221
+ }
1222
+
1223
+ /* Everything else maps directly: _root_, (group), [param], regular segments */
1224
+ pathParts.push(seg);
1225
+ }
1226
+
1227
+ /* index as last segment → trailing / (index route convention) */
1228
+ if (pathParts.length > 0 && pathParts[pathParts.length - 1] === "index") {
1229
+ pathParts[pathParts.length - 1] = "";
1230
+ }
1231
+
1232
+ return pathParts.join("/");
1233
+ }
1234
+
1235
+ function isRouteConventionDir(seg: string): boolean {
1236
+ return /^_[^_].*[^_]_$|^__[^_].*[^_]__$/.test(seg);
1237
+ }
1238
+
1239
+ function hasRootScope(virtualPath: string): boolean {
1240
+ return virtualPath.split("/").some((seg) => ROOT_SCOPE_RE.test(seg) && seg.length >= 3);
1241
+ }
1242
+
1243
+ function isIgnoredDirSegment(seg: string, ignorePrefix: string): boolean {
1244
+ return seg.startsWith(ignorePrefix) && !isRouteConventionDir(seg) && !seg.startsWith("[_]");
1245
+ }
1246
+
1247
+ /**
1248
+ * Inspect a project tree against the filesystem-codegen convention.
1249
+ * `fsVirtualPaths: true` requires suffix files under `src/routes/_name_/`.
1250
+ */
1251
+ export function inspectFsCodegenLayout(options: ScanOptions): FsLayoutIssue[] {
1252
+ const srcDir = options.srcDir ?? "src";
1253
+ const srcPath = join(options.rootDir, srcDir);
1254
+ const prefix = options.ignorePrefix ?? "_";
1255
+ const issues: FsLayoutIssue[] = [];
1256
+
1257
+ let entries: string[];
1258
+ try {
1259
+ entries = readdirSync(srcPath, { recursive: true }) as string[];
1260
+ } catch {
1261
+ return issues;
1262
+ }
1263
+
1264
+ for (const entry of entries) {
1265
+ const rel = entry.replace(/\\/g, "/");
1266
+ if (!rel.endsWith(".ts") && !rel.endsWith(".tsx") && !rel.endsWith(".js") && !rel.endsWith(".jsx")) continue;
1267
+ if (rel.includes("_gen/") || rel.includes("node_modules")) continue;
1268
+ if (rel.endsWith(".gen.ts") || rel.endsWith(".gen.tsx")) continue;
1269
+
1270
+ const segments = rel.split("/");
1271
+ const fileName = segments[segments.length - 1] ?? "";
1272
+ const dirs = segments.slice(0, -1);
1273
+ if (dirs.some((seg) => isIgnoredDirSegment(seg, prefix))) continue;
1274
+
1275
+ const filePath = posix.join(srcDir, rel);
1276
+ const type = detectRouteType(fileName);
1277
+ const underRoutes = rel.startsWith("routes/");
1278
+
1279
+ if (type) {
1280
+ if (!underRoutes) {
1281
+ issues.push({
1282
+ filePath,
1283
+ kind: "suffix-outside-routes",
1284
+ message: `uses the .${type} suffix but is not under ${srcDir}/routes/`,
1285
+ });
1286
+ continue;
1287
+ }
1288
+ const rawPath = deriveVirtualPath(rel, "routes");
1289
+ if (!rawPath) continue;
1290
+ if (!hasRootScope(rawPath)) {
1291
+ issues.push({
1292
+ filePath,
1293
+ kind: "missing-root-scope",
1294
+ message: `is not inside a _name_ root-scope folder (move to ${srcDir}/routes/_root_/... or ${srcDir}/routes/_admin_/...)`,
1295
+ });
1296
+ }
1297
+ continue;
1298
+ }
1299
+
1300
+ if (!underRoutes) continue;
1301
+
1302
+ const fullPath = posix.join(srcPath, rel);
1303
+ let content: string;
1304
+ try {
1305
+ content = readFileSync(fullPath, "utf-8");
1306
+ } catch {
1307
+ continue;
1308
+ }
1309
+ const match = BUILDER_EXPORT_RE.exec(content);
1310
+ const builder = match?.[1];
1311
+ if (!builder) continue;
1312
+ const line = content.slice(0, match?.index ?? 0).split("\n").length;
1313
+ issues.push({
1314
+ filePath,
1315
+ kind: "string-style-route",
1316
+ line,
1317
+ message: `declares ${builder}(...) but has no .page/.layout/.root-layout/.path-segment suffix — skipped`,
1318
+ });
1319
+ }
1320
+
1321
+ return issues;
1322
+ }
1323
+
1324
+ export function formatFsCodegenError(issues: FsLayoutIssue[]): string {
1325
+ const groups: Record<FsLayoutIssueKind, FsLayoutIssue[]> = {
1326
+ "missing-root-scope": [],
1327
+ "string-style-route": [],
1328
+ "suffix-outside-routes": [],
1329
+ };
1330
+ for (const issue of issues) groups[issue.kind].push(issue);
1331
+
1332
+ const lines = ["fsVirtualPaths is enabled, but the project does not follow the filesystem route convention.", ""];
1333
+
1334
+ if (groups["string-style-route"].length > 0) {
1335
+ lines.push("String-style route files (invisible to filesystem codegen):");
1336
+ for (const issue of groups["string-style-route"]) {
1337
+ lines.push(` - ${formatFsLayoutWarning(issue)}`);
1338
+ }
1339
+ lines.push("");
1340
+ }
1341
+ if (groups["missing-root-scope"].length > 0) {
1342
+ lines.push("Route files outside a _name_ root scope:");
1343
+ for (const issue of groups["missing-root-scope"]) {
1344
+ lines.push(` - ${formatFsLayoutWarning(issue)}`);
1345
+ }
1346
+ lines.push("");
1347
+ }
1348
+ if (groups["suffix-outside-routes"].length > 0) {
1349
+ lines.push("Suffix route files outside src/routes/:");
1350
+ for (const issue of groups["suffix-outside-routes"]) {
1351
+ lines.push(` - ${formatFsLayoutWarning(issue)}`);
1352
+ }
1353
+ lines.push("");
1354
+ }
1355
+
1356
+ lines.push(
1357
+ "Expected layout:",
1358
+ ' src/routes/_root_/root.root-layout.tsx → createRootLayout("_root_")',
1359
+ ' src/routes/_root_/home.page.tsx → createPage("_root_/")',
1360
+ ' src/routes/_root_/about/about.page.tsx → createPage("_root_/about")',
1361
+ ' src/routes/_root_/(blog)/blog.layout.tsx → createLayout("_root_/(blog)")',
1362
+ ' src/routes/_root_/users/[id]/user.page.tsx → createPage("_root_/users/[id]")',
1363
+ "",
1364
+ "Directories starting with _ (except _name_ root scopes) are ignored.",
1365
+ "Use [_]name for a literal _name URL segment.",
1366
+ "",
1367
+ "To keep writing virtual paths yourself:",
1368
+ " flare({ codegen: { fsVirtualPaths: false } })",
1369
+ );
1370
+
1371
+ return lines.join("\n");
1372
+ }
1373
+
1374
+ /** Write or update the route builder declaration in a source file. */
1375
+ export function writeRouteDeclaration(filePath: string, virtualPath: string, routeType: RouteDefinition["type"]): void {
1376
+ const builderFn = BUILDER_MAP[routeType];
1377
+ const content = readFileSync(filePath, "utf-8");
1378
+
1379
+ const newDeclLine = `export const route = ${builderFn}("${escapeCodegenStr(virtualPath)}")`;
1380
+
1381
+ /* Check for existing builder call to update (matches any builder type for rename support) */
1382
+ const anyBuilderRe =
1383
+ /export const route = (createPage|createLayout|createRootLayout|createPathSegment)\s*(?:<[^>]*>)?\s*\(\s*["'`][^"'`]*["'`]\)/;
1384
+ const match = anyBuilderRe.exec(content);
1385
+
1386
+ if (match) {
1387
+ /* Path and builder unchanged — skip write */
1388
+ if (match[0] === newDeclLine) return;
1389
+
1390
+ /* Also fix import if builder type changed */
1391
+ const oldBuilderFn = match[1] ?? "";
1392
+ let updated = content.slice(0, match.index) + newDeclLine + content.slice(match.index + match[0].length);
1393
+ if (oldBuilderFn !== builderFn) {
1394
+ updated = updated.replace(
1395
+ new RegExp(
1396
+ `import\\s*\\{\\s*${oldBuilderFn}\\s*\\}\\s*from\\s*["'\`](?:@lovrozagar/)?flare(?:/[\\w-]+)?["'\`]`,
1397
+ ),
1398
+ `import { ${builderFn} } from "${builderSubpath(builderFn)}"`,
1399
+ );
1400
+ }
1401
+ writeFileSync(filePath, updated, "utf-8");
1402
+ return;
1403
+ }
1404
+
1405
+ /* Missing declaration — prepend import + builder call, preserve existing content */
1406
+ const header = [
1407
+ `import { ${builderFn} } from "${builderSubpath(builderFn)}"`,
1408
+ "",
1409
+ "/* @flare-generated */",
1410
+ newDeclLine,
1411
+ ].join("\n");
1412
+
1413
+ const trimmed = content.trim();
1414
+ const output = trimmed.length === 0 ? `${header}\n` : `${header}\n${trimmed}\n`;
1415
+ writeFileSync(filePath, output, "utf-8");
1416
+ }
1417
+
1418
+ /** Scan routes/ for filesystem-based route files and return RouteDefinitions. */
1419
+ export function scanSourceFilesFsCodegen(options: ScanOptions): RouteDefinition[] {
1420
+ const srcPath = join(options.rootDir, options.srcDir ?? "src");
1421
+ const routesDir = "routes";
1422
+ const defs: RouteDefinition[] = [];
1423
+
1424
+ let entries: string[];
1425
+ try {
1426
+ entries = readdirSync(srcPath, { recursive: true }) as string[];
1427
+ } catch {
1428
+ return defs;
1429
+ }
1430
+
1431
+ for (const entry of entries) {
1432
+ const rel = entry.replace(/\\/g, "/");
1433
+
1434
+ /* Only process files under routes/ */
1435
+ if (!rel.startsWith(`${routesDir}/`)) continue;
1436
+
1437
+ /* Skip generated files */
1438
+ if (rel.includes("_gen/") || rel.endsWith(".gen.ts") || rel.endsWith(".gen.tsx")) continue;
1439
+
1440
+ /* Detect route type from suffix */
1441
+ const fileName = rel.split("/").pop() ?? "";
1442
+ const type = detectRouteType(fileName);
1443
+ if (!type) continue;
1444
+
1445
+ /* Derive virtual path from folder position */
1446
+ const rawPath = deriveVirtualPath(rel, routesDir);
1447
+ if (!rawPath) continue;
1448
+
1449
+ /* Pages at a root convention boundary get trailing / to distinguish from layouts.
1450
+ * e.g. _root_/home.page.tsx → _root_/ (variable path /), not _root_ (layout namespace) */
1451
+ const isRootBoundary = type === "page" && /^(\[[^\]]+\]\/)*_[^/]+_$/.test(rawPath);
1452
+ const virtualPath = isRootBoundary ? `${rawPath}/` : rawPath;
1453
+
1454
+ /* Read file content for chain method detection */
1455
+ const fullPath = posix.join(srcPath, rel);
1456
+ let content: string;
1457
+ try {
1458
+ content = readFileSync(fullPath, "utf-8");
1459
+ } catch {
1460
+ continue;
1461
+ }
1462
+
1463
+ /* Extract chain metadata from file content */
1464
+ const hasAuthenticateOptional = /\.authenticateOptional\s*\(/.test(content);
1465
+ const hasAuthenticate = /\.authenticate\s*\(/.test(content);
1466
+ const authenticateMode: AuthenticateMode = hasAuthenticateOptional ? "optional" : !!hasAuthenticate;
1467
+ const hasInput = /\.input\s*(?:<[^>]*>)?\s*\(/.test(content);
1468
+ const responseRoute = /\.response\s*\(/.test(content);
1469
+ const cache = extractCacheFromChain(content);
1470
+ const intercept = extractInterceptFromChain(content);
1471
+
1472
+ defs.push({
1473
+ authenticateMode,
1474
+ cache,
1475
+ exportName: "route",
1476
+ filePath: posix.join(options.srcDir ?? "src", rel),
1477
+ hasInput,
1478
+ intercept,
1479
+ responseRoute,
1480
+ type,
1481
+ virtualPath,
1482
+ });
1483
+ }
1484
+
1485
+ return defs;
1486
+ }
1487
+
1488
+ export interface RunGenerateOptions {
1489
+ fsCodegen?: boolean;
1490
+ ignorePrefix?: string;
1491
+ outputPath?: string;
1492
+ rootDir: string;
1493
+ srcDir?: string;
1494
+ typesOutputPath?: string;
1495
+ }
1496
+
1497
+ export function runGenerate(options: RunGenerateOptions): GenerateResult {
1498
+ const outputPath = options.outputPath ?? "src/_gen/routes.gen.ts";
1499
+ const outputDir = dirname(outputPath);
1500
+ const warnings: string[] = [];
1501
+ const skipFiles = new Set<string>();
1502
+
1503
+ /* fsCodegen pre-step: derive virtual paths from filesystem and write into source files */
1504
+ if (options.fsCodegen) {
1505
+ const layoutIssues = inspectFsCodegenLayout({
1506
+ ignorePrefix: options.ignorePrefix,
1507
+ rootDir: options.rootDir,
1508
+ srcDir: options.srcDir,
1509
+ });
1510
+ for (const issue of layoutIssues) skipFiles.add(issue.filePath);
1511
+
1512
+ const fsDefs = scanSourceFilesFsCodegen({
1513
+ rootDir: options.rootDir,
1514
+ srcDir: options.srcDir,
1515
+ }).filter((def) => !skipFiles.has(def.filePath));
1516
+
1517
+ /* No usable suffix routes + layout issues → the whole tree is wrong. */
1518
+ if (fsDefs.length === 0 && layoutIssues.length > 0) {
1519
+ throw new Error(formatFsCodegenError(layoutIssues));
1520
+ }
1521
+
1522
+ for (const issue of layoutIssues) {
1523
+ const warning = formatFsLayoutWarning(issue);
1524
+ warnings.push(warning);
1525
+ console.warn(`[flare:generate] ${warning}`);
1526
+ }
1527
+
1528
+ for (const def of fsDefs) {
1529
+ const fullPath = join(options.rootDir, def.filePath);
1530
+ writeRouteDeclaration(fullPath, def.virtualPath, def.type);
1531
+ }
1532
+
1533
+ const hasPage = fsDefs.some((d) => d.type === "page");
1534
+ const hasRootLayout = fsDefs.some((d) => d.type === "root-layout");
1535
+ if (hasPage && !hasRootLayout) {
1536
+ const warning =
1537
+ "no *.root-layout.tsx found — pages will render without a root layout (expected src/routes/_root_/*.root-layout.tsx)";
1538
+ warnings.push(warning);
1539
+ console.warn(`[flare:generate] ${warning}`);
1540
+ }
1541
+ }
1542
+
1543
+ /* Always scan via regex — reads the createPage("...") strings from source files */
1544
+ let defs = scanSourceFiles({
1545
+ ignorePrefix: options.ignorePrefix,
1546
+ rootDir: options.rootDir,
1547
+ srcDir: options.srcDir,
1548
+ });
1549
+
1550
+ /* fs mode only emits suffix files that passed layout inspection */
1551
+ if (options.fsCodegen) {
1552
+ defs = defs.filter((def) => {
1553
+ if (skipFiles.has(def.filePath)) return false;
1554
+ const name = def.filePath.split("/").pop() ?? "";
1555
+ return detectRouteType(name) !== null;
1556
+ });
1557
+ }
1558
+
1559
+ const errors = validateRouteDefinitions(defs);
1560
+ if (errors.length > 0) {
1561
+ throw new Error(`Route validation failed:\n${errors.join("\n")}`);
1562
+ }
1563
+
1564
+ const fullOutputPath = join(options.rootDir, outputPath);
1565
+ const virtualTypesPath = options.typesOutputPath
1566
+ ? join(options.rootDir, options.typesOutputPath)
1567
+ : join(dirname(fullOutputPath), "types.gen.d.ts");
1568
+
1569
+ /* Library packages and empty folders are not Flare apps — do not leave a stub _gen. */
1570
+ if (defs.length === 0) {
1571
+ for (const leftover of [fullOutputPath, virtualTypesPath]) {
1572
+ if (existsSync(leftover)) unlinkSync(leftover);
1573
+ }
1574
+ return { layouts: 0, routes: 0, warnings };
1575
+ }
1576
+
1577
+ const content = generateRoutesFile(defs, outputDir);
1578
+ writeGenFile(fullOutputPath, content, "flare");
1579
+
1580
+ /* Write virtual module type declarations as a .d.ts file (must be ambient, not in a module) */
1581
+ writeGenFile(virtualTypesPath, generateVirtualModuleTypes(), "flare");
1582
+
1583
+ const routes = defs.filter((d) => d.type === "page").length;
1584
+ const layouts = defs.filter(
1585
+ (d) => d.type === "layout" || d.type === "root-layout" || d.type === "path-segment",
1586
+ ).length;
1587
+
1588
+ return { layouts, routes, warnings };
1589
+ }