@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,1278 @@
1
+ import { batch } from "solid-js";
2
+ import type { DeferredTracker } from "../caches/index.ts";
3
+ import { collectDeferredPromises, createDeferredTracker } from "../caches/index.ts";
4
+ import type { DirectionConfig } from "../direction.ts";
5
+ import { getDirFromLocale } from "../direction.ts";
6
+ import { parseMilliseconds } from "../duration/index.ts";
7
+ import { NotFoundError, RedirectResponse } from "../errors/index.ts";
8
+ import type { PerRouteHead } from "../head-client/index.ts";
9
+ import { applyPerRouteHeads } from "../head-client/index.ts";
10
+ import {
11
+ createHistoryListener,
12
+ createScrollStore,
13
+ getCurrentScroll,
14
+ getHistoryIndex,
15
+ type HistoryNavigateEvent,
16
+ incrementHistoryIndex,
17
+ parseHistoryState,
18
+ pushHistoryState,
19
+ replaceHistoryState,
20
+ restoreScroll,
21
+ type ScrollStore,
22
+ scrollToTop,
23
+ setHistoryIndex,
24
+ } from "../history/index.ts";
25
+ import { isChunkLoadError, isRenderFn } from "../internal.ts";
26
+ import type { LocaleConfig } from "../locale.ts";
27
+ import { warn } from "../logger.ts";
28
+ import { fetchNDJSON } from "../ndjson-client/index.ts";
29
+ import type {
30
+ FlareProviderContext,
31
+ InternalNavigateOptions,
32
+ LocationChangeInfo,
33
+ ViewTransitionConfig,
34
+ ViewTransitionDirection,
35
+ } from "../outlet/types.ts";
36
+ import { executeRewriteInput, executeRewriteOutput, type LocationRewrite } from "../rewrite/index.ts";
37
+ import type { HeadConfig } from "../route-builder/types.ts";
38
+ import {
39
+ computeMatchId,
40
+ isRootLayoutPath,
41
+ matchRoute,
42
+ matchRoutePartial,
43
+ toLocaleMatch,
44
+ } from "../router-primitives/index.ts";
45
+ import { buildUrl, parseSearchParams, type SearchParams, serializeSearchParams } from "../url/index.ts";
46
+ import type { LoadedRouteModule, LoadRouteModulesFn } from "./types.ts";
47
+
48
+ function extractRootIdentity(virtualPath: string): string {
49
+ const segments = virtualPath.split("/");
50
+ const rootIdx = segments.findIndex((s) => isRootLayoutPath(s));
51
+ if (rootIdx < 0) return "";
52
+ return segments.slice(0, rootIdx + 1).join("/");
53
+ }
54
+
55
+ /** FlareProviderContext with hidden setters defined via Object.defineProperty in outlet */
56
+ interface InternalProviderContext extends FlareProviderContext {
57
+ _setNavigate?: (fn: (opts: InternalNavigateOptions) => Promise<void>) => void;
58
+ _setPrefetch?: (
59
+ fn: (opts: { params?: Record<string, unknown>; search?: Record<string, unknown>; to: string }) => Promise<void>,
60
+ ) => void;
61
+ }
62
+
63
+ /** Document with View Transitions API support (types arg form not yet in TS DOM lib) */
64
+ interface ViewTransitionResult {
65
+ finished: Promise<void>;
66
+ ready: Promise<void>;
67
+ skipTransition: () => void;
68
+ updateCallbackDone: Promise<void>;
69
+ }
70
+
71
+ interface ViewTransitionDocument {
72
+ startViewTransition: (arg: (() => void) | { types?: string[]; update: () => void }) => ViewTransitionResult;
73
+ }
74
+
75
+ function hasViewTransitions(doc: Document): doc is Document & ViewTransitionDocument {
76
+ return "startViewTransition" in doc && typeof doc.startViewTransition === "function";
77
+ }
78
+
79
+ export type { EffectsConfig, LoadedRouteModule, LoadedRouteModules, LoadRouteModulesFn } from "./types.ts";
80
+
81
+ const GC_INTERVAL = 60_000;
82
+ const MAX_REDIRECTS = 10;
83
+ const GC_MAX_AGE = 5 * 60 * 1000;
84
+
85
+ /** Apply input rewrite to transform browser URL → internal pathname for route matching */
86
+ function rewritePathname(pathname: string): string {
87
+ if (!rewrite) return pathname;
88
+ const url = new URL(pathname, "http://localhost");
89
+ const rewritten = executeRewriteInput(rewrite, url);
90
+ return rewritten.pathname;
91
+ }
92
+
93
+ let ctx: FlareProviderContext | null = null;
94
+ let currentController: AbortController | null = null;
95
+ let navigationVersion = 0;
96
+ let scrollStore: ScrollStore | null = null;
97
+ let gcIntervalId: ReturnType<typeof setInterval> | null = null;
98
+ let popstateCleanup: (() => void) | null = null;
99
+ let loadRouteModules: LoadRouteModulesFn | null = null;
100
+ let currentHistoryKey: string | null = null;
101
+ let rewrite: LocationRewrite | undefined;
102
+ let caseSensitive = false;
103
+ let defaultViewTransition: ViewTransitionConfig = false;
104
+ let deferredTracker: DeferredTracker | null = null;
105
+ let notFoundMode: "fuzzy" | "root" = "fuzzy";
106
+ let queryClientRef: unknown;
107
+ let scrollRestorationEnabled = true;
108
+ let scrollRestorationBehavior: "auto" | "smooth" = "auto";
109
+ let localeConfig: LocaleConfig | undefined;
110
+ let directionConfig: DirectionConfig | undefined;
111
+ const visitedRoutes = new Set<string>();
112
+
113
+ /* Keepalive ping — keeps HTTP/2 connection and CF isolate warm */
114
+ let keepaliveIntervalId: ReturnType<typeof setInterval> | null = null;
115
+ let keepaliveVisibilityHandler: (() => void) | null = null;
116
+
117
+ let clickCleanup: (() => void) | null = null;
118
+
119
+ /* SPA navigation blocker — set by useBlocker, checked by navigate */
120
+ let activeBlockerFn: (() => boolean) | null = null;
121
+ let onBlockedCallback: (() => void) | null = null;
122
+ let pendingNavigation: InternalNavigateOptions | null = null;
123
+
124
+ /** Reset navigation phase + clear controller in one place — used at all early-return sites */
125
+ function stopNavigation(): void {
126
+ if (ctx) {
127
+ ctx.setNavigationPhase("idle");
128
+ ctx.setViewTransition(null);
129
+ }
130
+ currentController = null;
131
+ }
132
+
133
+ /**
134
+ * Sync locale state (cookie, html lang, direction) on navigation commit.
135
+ * Called at every c.setParams() site — these only fire during actual
136
+ * navigations, never during hydration (signals are initialized from SSR state).
137
+ */
138
+ function syncLocale(params: Record<string, string | string[]>): void {
139
+ if (!localeConfig || typeof document === "undefined") return;
140
+
141
+ const paramName = localeConfig.paramName ?? "locale";
142
+ const val = params[paramName];
143
+ const effectiveLocale = (typeof val === "string" ? val : undefined) ?? localeConfig.defaultLocale;
144
+
145
+ /* Cookie — always write for localized routes to keep it current */
146
+ const cookieName = localeConfig.cookieName ?? "flare.locale";
147
+ document.cookie = `${cookieName}=${effectiveLocale}; path=/; max-age=31536000; samesite=lax`;
148
+
149
+ /* html lang */
150
+ document.documentElement.setAttribute("lang", effectiveLocale);
151
+
152
+ /* Direction sync (opt-in) */
153
+ if (localeConfig.syncDirection) {
154
+ const dir = getDirFromLocale(effectiveLocale, directionConfig?.rtlLocales);
155
+ document.documentElement.setAttribute("dir", dir);
156
+ const attr = directionConfig?.attribute ?? "data-dir";
157
+ document.documentElement.setAttribute(attr, dir);
158
+ }
159
+ }
160
+
161
+ export function setActiveBlocker(when: (() => boolean) | null, onBlocked?: () => void): void {
162
+ activeBlockerFn = when;
163
+ onBlockedCallback = onBlocked ?? null;
164
+ }
165
+
166
+ export function clearPendingNavigation(): void {
167
+ pendingNavigation = null;
168
+ }
169
+
170
+ export function proceedPendingNavigation(): void {
171
+ const pending = pendingNavigation;
172
+ pendingNavigation = null;
173
+ if (pending) {
174
+ void navigate({ ...pending, _bypassBlocker: true });
175
+ }
176
+ }
177
+
178
+ export interface SetupNavigationOptions {
179
+ caseSensitive?: boolean;
180
+ direction?: DirectionConfig;
181
+ initialRouteIds?: string[];
182
+ keepalive?: false | number;
183
+ locale?: LocaleConfig;
184
+ notFoundMode?: "fuzzy" | "root";
185
+ queryClient?: unknown;
186
+ rewrite?: LocationRewrite;
187
+ scrollRestoration?: boolean;
188
+ scrollRestorationBehavior?: "auto" | "smooth";
189
+ scrollRestorationMaxEntries?: number;
190
+ viewTransitions?: ViewTransitionConfig;
191
+ }
192
+
193
+ function setupAnchorIntercept(): () => void {
194
+ if (typeof document === "undefined") return () => {};
195
+
196
+ const handler = (event: MouseEvent): void => {
197
+ if (event.defaultPrevented) return;
198
+ if (event.button !== 0) return;
199
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
200
+
201
+ const target = event.target;
202
+ if (!(target instanceof Element)) return;
203
+ const anchor = target.closest("a");
204
+ if (!anchor) return;
205
+ if (anchor.hasAttribute("data-flare-skip")) return;
206
+
207
+ const href = anchor.getAttribute("href");
208
+ if (href === null) return;
209
+
210
+ const anchorTarget = anchor.getAttribute("target");
211
+ if (anchorTarget && anchorTarget !== "_self") return;
212
+ if (anchor.hasAttribute("download")) return;
213
+ const rel = anchor.getAttribute("rel");
214
+ if (rel && rel.split(/\s+/).includes("external")) return;
215
+
216
+ /* Dangerous + non-http protocols — let browser handle. mailto/tel/javascript/data/blob/vbscript. */
217
+ if (isExternal(href)) return;
218
+ const lower = href.trim().toLowerCase();
219
+ if (
220
+ lower.startsWith("javascript:") ||
221
+ lower.startsWith("data:") ||
222
+ lower.startsWith("blob:") ||
223
+ lower.startsWith("vbscript:")
224
+ )
225
+ return;
226
+
227
+ let url: URL;
228
+ try {
229
+ url = new URL(href, window.location.href);
230
+ } catch {
231
+ return;
232
+ }
233
+ /* same-origin only — isExternal already covers mailto/tel and cross-origin http(s) */
234
+ if (url.origin !== window.location.origin) return;
235
+
236
+ if (!ctx) return;
237
+ const matchPath = rewritePathname(url.pathname);
238
+ const match = matchRoute(ctx.routeTree, matchPath, caseSensitive, toLocaleMatch(localeConfig));
239
+ if (!match) return;
240
+
241
+ event.preventDefault();
242
+ /* hash passed separately — buildUrl appends it; if `to` also carried it we'd double-apply */
243
+ void navigate({
244
+ _precomputedMatch: match,
245
+ hash: url.hash || undefined,
246
+ to: url.pathname + url.search,
247
+ });
248
+ };
249
+
250
+ /* bubble phase on document — capture handlers (incl. external blockers) fire first, then we handle unclaimed same-origin anchors */
251
+ document.addEventListener("click", handler);
252
+ return () => document.removeEventListener("click", handler);
253
+ }
254
+
255
+ export function setupNavigation(
256
+ providerCtx: FlareProviderContext,
257
+ loadModulesFn: LoadRouteModulesFn,
258
+ initialRouteIdsOrOptions?: SetupNavigationOptions | string[],
259
+ ): void {
260
+ ctx = providerCtx;
261
+ loadRouteModules = loadModulesFn;
262
+
263
+ const options: SetupNavigationOptions = Array.isArray(initialRouteIdsOrOptions)
264
+ ? { initialRouteIds: initialRouteIdsOrOptions }
265
+ : (initialRouteIdsOrOptions ?? {});
266
+
267
+ scrollStore = createScrollStore(options.scrollRestorationMaxEntries);
268
+ deferredTracker = createDeferredTracker(providerCtx.matchCache);
269
+
270
+ rewrite = options.rewrite;
271
+ caseSensitive = options.caseSensitive ?? false;
272
+
273
+ defaultViewTransition = options.viewTransitions ?? false;
274
+ localeConfig = options.locale;
275
+ directionConfig = options.direction;
276
+ notFoundMode = options.notFoundMode ?? "fuzzy";
277
+ queryClientRef = options.queryClient;
278
+ scrollRestorationEnabled = options.scrollRestoration ?? true;
279
+ scrollRestorationBehavior = options.scrollRestorationBehavior ?? "auto";
280
+
281
+ /* Pre-populate visitedRoutes with hydrated page's route IDs */
282
+ if (options.initialRouteIds) {
283
+ for (const id of options.initialRouteIds) {
284
+ visitedRoutes.add(id);
285
+ }
286
+ }
287
+
288
+ if (scrollRestorationEnabled && typeof history !== "undefined" && "scrollRestoration" in history) {
289
+ history.scrollRestoration = "manual";
290
+ }
291
+
292
+ /* Track current history key for scroll save on popstate */
293
+ const initialState = parseHistoryState(typeof history !== "undefined" ? history.state : null);
294
+ currentHistoryKey = initialState?.key ?? null;
295
+
296
+ /* Bind navigate + prefetch on context via hidden setters */
297
+ const internal = ctx as InternalProviderContext;
298
+ if (internal._setNavigate) internal._setNavigate(navigate);
299
+ if (internal._setPrefetch) internal._setPrefetch(prefetch);
300
+
301
+ /* Popstate listener — clean up previous before re-registering */
302
+ if (popstateCleanup) popstateCleanup();
303
+ popstateCleanup = createHistoryListener((event: HistoryNavigateEvent) => {
304
+ /* Intercept dismissal: if we're currently intercepted and the new state
305
+ * doesn't have the _intercepted marker, clear the intercept overlay.
306
+ * The background route is still rendered — just dismiss the overlay. */
307
+ const eventState = event.state as Record<string, unknown> | null | undefined;
308
+ if (ctx?.intercepted() && !(eventState && eventState._intercepted === true)) {
309
+ ctx.setIntercepted(null);
310
+ setHistoryIndex(event.historyIndex);
311
+ currentHistoryKey = event.key;
312
+ stopNavigation();
313
+ return;
314
+ }
315
+
316
+ /* Compute direction before updating index */
317
+ const prevIndex = getHistoryIndex();
318
+ let direction: ViewTransitionDirection = "same";
319
+ if (event.historyIndex > prevIndex) direction = "forward";
320
+ else if (event.historyIndex < prevIndex) direction = "back";
321
+ setHistoryIndex(event.historyIndex);
322
+
323
+ /* Save scroll for the page we're leaving (currentHistoryKey is still the old page) */
324
+ if (scrollRestorationEnabled && scrollStore && currentHistoryKey) {
325
+ scrollStore.save(currentHistoryKey, getCurrentScroll());
326
+ }
327
+
328
+ /* Update tracked key to destination */
329
+ currentHistoryKey = event.key;
330
+
331
+ const restorePos = scrollStore?.get(event.key) ?? null;
332
+
333
+ navigate({
334
+ _popstate: true,
335
+ _popstateDirection: direction,
336
+ _restoreScroll: restorePos,
337
+ scroll: false,
338
+ to: typeof window !== "undefined" ? window.location.href : "/",
339
+ });
340
+ });
341
+
342
+ /* Garbage collection interval — clear any existing before creating new */
343
+ if (gcIntervalId !== null) clearInterval(gcIntervalId);
344
+ gcIntervalId = setInterval(() => {
345
+ if (!ctx) return;
346
+ const now = Date.now();
347
+ for (const entry of ctx.matchCache.getAll()) {
348
+ if (now - entry.updatedAt > GC_MAX_AGE) {
349
+ ctx.matchCache.delete(entry.matchId);
350
+ }
351
+ }
352
+ ctx.prefetchCache.cleanup(GC_MAX_AGE);
353
+ }, GC_INTERVAL);
354
+
355
+ /* Keepalive ping — clear previous before setting up */
356
+ if (keepaliveIntervalId !== null) clearInterval(keepaliveIntervalId);
357
+ if (keepaliveVisibilityHandler) {
358
+ document.removeEventListener("visibilitychange", keepaliveVisibilityHandler);
359
+ keepaliveVisibilityHandler = null;
360
+ }
361
+
362
+ const keepaliveMs = options.keepalive;
363
+ if (typeof keepaliveMs === "number" && keepaliveMs > 0) {
364
+ const ping = () => {
365
+ fetch("/_flare/keepalive", { priority: "low" }).catch(() => {});
366
+ };
367
+
368
+ keepaliveIntervalId = setInterval(ping, keepaliveMs);
369
+
370
+ keepaliveVisibilityHandler = () => {
371
+ if (document.visibilityState === "hidden") {
372
+ if (keepaliveIntervalId !== null) {
373
+ clearInterval(keepaliveIntervalId);
374
+ keepaliveIntervalId = null;
375
+ }
376
+ } else {
377
+ if (keepaliveIntervalId !== null) clearInterval(keepaliveIntervalId);
378
+ ping();
379
+ keepaliveIntervalId = setInterval(ping, keepaliveMs);
380
+ }
381
+ };
382
+ document.addEventListener("visibilitychange", keepaliveVisibilityHandler);
383
+ }
384
+
385
+ /* Anchor intercept — clear previous before re-installing (HMR / re-init safety) */
386
+ if (clickCleanup) clickCleanup();
387
+ clickCleanup = setupAnchorIntercept();
388
+ }
389
+
390
+ /** Resolve URL from navigate options. Returns null if same-URL guard triggers. */
391
+ function resolveNavigationUrl(options: InternalNavigateOptions): URL | null {
392
+ const resolvedPath = buildUrl({
393
+ hash: options.hash,
394
+ params: options.params,
395
+ search: options.search,
396
+ to: options.to,
397
+ });
398
+ const url = new URL(resolvedPath, typeof window !== "undefined" ? window.location.href : "http://localhost/");
399
+
400
+ /* Same-URL guard (popstate always proceeds — browser already changed URL) */
401
+ if (typeof window !== "undefined" && url.href === window.location.href && !options.revalidate && !options._popstate) {
402
+ return null;
403
+ }
404
+
405
+ return url;
406
+ }
407
+
408
+ /** Save scroll position + push/replace history state. */
409
+ function handleHistoryUpdate(
410
+ options: InternalNavigateOptions,
411
+ url: URL,
412
+ params: Record<string, string | string[]>,
413
+ ): void {
414
+ if (scrollRestorationEnabled && scrollStore && currentHistoryKey) {
415
+ scrollStore.save(currentHistoryKey, getCurrentScroll());
416
+ }
417
+
418
+ let newState: ReturnType<typeof pushHistoryState>;
419
+ if (options.replace) {
420
+ newState = replaceHistoryState(url.pathname, params, url.search, {
421
+ hash: url.hash,
422
+ historyIndex: getHistoryIndex(),
423
+ state: options.state,
424
+ });
425
+ } else {
426
+ incrementHistoryIndex();
427
+ newState = pushHistoryState(url.pathname, params, url.search, {
428
+ hash: url.hash,
429
+ historyIndex: getHistoryIndex(),
430
+ state: options.state,
431
+ });
432
+ }
433
+ currentHistoryKey = newState.key;
434
+ }
435
+
436
+ /** Map loaded modules to client match objects using cached data. */
437
+ function buildClientMatches(
438
+ allModules: LoadedRouteModule[],
439
+ search: SearchParams,
440
+ params: Record<string, string | string[]>,
441
+ ) {
442
+ return allModules.map((mod) => {
443
+ const deps = mod.effectsConfig?.loaderDeps?.({ search }) ?? [];
444
+ const matchId = computeMatchId({
445
+ loaderDeps: () => deps,
446
+ params,
447
+ routeId: mod.virtualPath,
448
+ search,
449
+ });
450
+ const cached = ctx?.matchCache.get(matchId);
451
+ const error = cached?.error instanceof Error ? cached.error : undefined;
452
+
453
+ return {
454
+ _type: mod._type,
455
+ error,
456
+ errorRender: isRenderFn(mod.errorRender) ? mod.errorRender : undefined,
457
+ loaderData: cached?.data ?? null,
458
+ notFoundRender: isRenderFn(mod.notFoundRender) ? mod.notFoundRender : undefined,
459
+ preloaderContext: cached?.preloaderContext,
460
+ render: mod.render,
461
+ unauthenticatedRender: isRenderFn(mod.unauthenticatedRender) ? mod.unauthenticatedRender : undefined,
462
+ unauthorizedRender: isRenderFn(mod.unauthorizedRender) ? mod.unauthorizedRender : undefined,
463
+ variablePath: mod.variablePath,
464
+ virtualPath: mod.virtualPath,
465
+ };
466
+ });
467
+ }
468
+
469
+ /** Apply view transition with VT API, or call update() directly as fallback.
470
+ * Returns a promise that resolves after update() has executed so navigate()
471
+ * callers can rely on state being settled when the promise resolves.
472
+ *
473
+ * Phase transitions: sets "transitioning" + exposes VT object after startViewTransition,
474
+ * wires finished → "idle". The update() callback (which calls stopNavigation)
475
+ * only handles the non-VT cleanup; VT cleanup is deferred to finished. */
476
+ async function applyViewTransition(
477
+ resolvedVT: ViewTransitionConfig,
478
+ update: () => void,
479
+ options: InternalNavigateOptions,
480
+ url: URL,
481
+ version: number,
482
+ ): Promise<void> {
483
+ const doc = typeof document !== "undefined" ? document : null;
484
+ if (doc && hasViewTransitions(doc) && resolvedVT) {
485
+ const startVT = doc.startViewTransition.bind(doc);
486
+
487
+ let transition: ViewTransitionResult | undefined;
488
+ try {
489
+ if (typeof resolvedVT === "object" && resolvedVT.types) {
490
+ const rawTypes = resolvedVT.types;
491
+ if (typeof rawTypes === "function") {
492
+ const direction: ViewTransitionDirection =
493
+ options._popstateDirection ?? (options._popstate ? "back" : "forward");
494
+ const fromLoc = ctx
495
+ ? {
496
+ hash: ctx.location().hash,
497
+ pathname: ctx.location().pathname,
498
+ search: serializeSearchParams(ctx.location().search),
499
+ }
500
+ : null;
501
+ const toLoc = { hash: url.hash, pathname: url.pathname, search: url.search };
502
+ const info: LocationChangeInfo = {
503
+ direction,
504
+ fromLocation: fromLoc,
505
+ pathChanged: fromLoc?.pathname !== toLoc.pathname,
506
+ toLocation: toLoc,
507
+ };
508
+ const result = rawTypes(info);
509
+ if (result === false) {
510
+ update();
511
+ stopNavigation();
512
+ return;
513
+ }
514
+ if (result.length > 0) {
515
+ transition = startVT({ types: result, update });
516
+ } else {
517
+ transition = startVT(update);
518
+ }
519
+ } else if (rawTypes.length > 0) {
520
+ transition = startVT({ types: rawTypes, update });
521
+ } else {
522
+ transition = startVT(update);
523
+ }
524
+ } else {
525
+ transition = startVT(update);
526
+ }
527
+ } catch (e: unknown) {
528
+ warn("nav", "view transition API failed", e);
529
+ update();
530
+ stopNavigation();
531
+ return;
532
+ }
533
+
534
+ if (transition) {
535
+ /**
536
+ * WebKit rejects transition.ready with AbortError when a new startViewTransition
537
+ * call replaces an in-flight one. Chromium swallows this internally but WebKit
538
+ * surfaces it as an unhandledrejection, which triggers the dev error overlay and
539
+ * blocks pointer events. The finished promise is already handled below.
540
+ */
541
+ transition.ready.catch(() => {});
542
+
543
+ await transition.updateCallbackDone;
544
+
545
+ /* State is settled — enter transitioning phase while VT animation plays */
546
+ if (ctx) {
547
+ ctx.setNavigationPhase("transitioning");
548
+ ctx.setViewTransition(transition);
549
+ }
550
+
551
+ /* Wire finished → idle (catch rejection too — VT can be skipped/aborted).
552
+ * Version check prevents stale VT from resetting phase when a new navigation superseded this one. */
553
+ transition.finished.then(
554
+ () => {
555
+ if (ctx && version === navigationVersion) {
556
+ ctx.setNavigationPhase("idle");
557
+ ctx.setViewTransition(null);
558
+ }
559
+ },
560
+ (e: unknown) => {
561
+ warn("nav", "view transition finished with error", e);
562
+ if (ctx && version === navigationVersion) {
563
+ ctx.setNavigationPhase("idle");
564
+ ctx.setViewTransition(null);
565
+ }
566
+ },
567
+ );
568
+ } else {
569
+ /* startVT returned void/undefined — update ran inside it, just clean up */
570
+ stopNavigation();
571
+ }
572
+ } else {
573
+ update();
574
+ stopNavigation();
575
+ }
576
+ }
577
+
578
+ export async function navigate(options: InternalNavigateOptions, redirectCount = 0): Promise<void> {
579
+ if (!ctx)
580
+ throw new Error(
581
+ "navigate() called before setupNavigation(). Call setupNavigation() in your root layout initialization.",
582
+ );
583
+ if (!loadRouteModules)
584
+ throw new Error("loadRouteModules not configured. Ensure the Flare Vite plugin is active in your vite.config.");
585
+ const c = ctx;
586
+
587
+ /* Step 0: Check SPA blocker (skip for popstate — browser already changed URL) */
588
+ if (activeBlockerFn && !options._popstate && !options._bypassBlocker && activeBlockerFn()) {
589
+ pendingNavigation = options;
590
+ /* Defer to avoid re-entrant navigation from SolidJS reactive effects */
591
+ if (onBlockedCallback) {
592
+ const cb = onBlockedCallback;
593
+ queueMicrotask(() => cb());
594
+ }
595
+ return;
596
+ }
597
+
598
+ /* Step 1: Resolve URL + same-URL guard */
599
+ const url = resolveNavigationUrl(options);
600
+ if (!url) return;
601
+
602
+ /* Step 2: Abort previous + set navigating */
603
+ if (currentController) currentController.abort();
604
+ const controller = new AbortController();
605
+ currentController = controller;
606
+ navigationVersion++;
607
+ const myVersion = navigationVersion;
608
+ ctx.setNavigationPhase("loading");
609
+
610
+ /* Step 3: Match route */
611
+ const matchPath = rewritePathname(url.pathname);
612
+ /* _precomputedMatch: anchor delegate already ran matchRoute — skip the second call */
613
+ const match =
614
+ options._precomputedMatch ?? matchRoute(ctx.routeTree, matchPath, caseSensitive, toLocaleMatch(localeConfig));
615
+ if (!match) {
616
+ if (notFoundMode === "fuzzy") {
617
+ const partial = matchRoutePartial(ctx.routeTree, matchPath, caseSensitive, toLocaleMatch(localeConfig));
618
+ if (partial && loadRouteModules) {
619
+ /* Build a pathname from the partial match that loadRouteModules can resolve */
620
+ const partialUrl = buildUrl({ params: partial.params, to: partial.route.v });
621
+ const modules = await loadRouteModules(partialUrl, ctx.routeTree, ctx.layouts).catch(() => null);
622
+
623
+ if (modules && !controller.signal.aborted && myVersion === navigationVersion) {
624
+ const search = parseSearchParams(url.searchParams);
625
+ const nonRootLayouts = modules.layouts.filter((m) => m._type !== "root-layout");
626
+ const clientMatches = buildClientMatches(nonRootLayouts, search, partial.params);
627
+ clientMatches.push({
628
+ _type: "render",
629
+ error: new NotFoundError(),
630
+ errorRender: undefined,
631
+ loaderData: null,
632
+ notFoundRender: undefined,
633
+ preloaderContext: undefined,
634
+ render: () => null,
635
+ unauthenticatedRender: undefined,
636
+ unauthorizedRender: undefined,
637
+ variablePath: "",
638
+ virtualPath: `${partial.route.x}/__notfound`,
639
+ });
640
+
641
+ if (!options._popstate) {
642
+ handleHistoryUpdate(options, url, partial.params);
643
+ }
644
+
645
+ batch(() => {
646
+ c.setNotFound(false);
647
+ c.setMatches(clientMatches);
648
+ c.setParams(partial.params);
649
+ c.setSearch(search);
650
+ stopNavigation();
651
+ });
652
+ syncLocale(partial.params);
653
+ applyPerRouteHeads([{ head: { title: "Not Found" }, matchId: `${partial.route.x}/__notfound` }]);
654
+ return;
655
+ }
656
+ }
657
+ }
658
+
659
+ if (!options._popstate) {
660
+ handleHistoryUpdate(options, url, {});
661
+ }
662
+
663
+ batch(() => {
664
+ c.setNotFound(true);
665
+ c.setMatches([]);
666
+ c.setParams({});
667
+ c.setSearch(parseSearchParams(url.searchParams));
668
+ stopNavigation();
669
+ });
670
+ applyPerRouteHeads([{ head: { title: "Not Found" }, matchId: "__notfound" }]);
671
+ return;
672
+ }
673
+
674
+ /* Step 3b: Cross-root detection */
675
+ const currentMatches = ctx.matches();
676
+ const currentRoot = extractRootIdentity(currentMatches[0]?.virtualPath ?? "");
677
+ const newRoot = extractRootIdentity(match.route.x);
678
+ if (currentRoot && newRoot && currentRoot !== newRoot) {
679
+ stopNavigation();
680
+ hardNavigate(url.href);
681
+ return;
682
+ }
683
+
684
+ /* Response route detection */
685
+ if (match.route.t === "x") {
686
+ stopNavigation();
687
+ hardNavigate(url.href);
688
+ return;
689
+ }
690
+
691
+ /* Intercept route detection: if target has intercept config and current variablePath
692
+ * matches one of the "from" routes, render as overlay instead of full navigation. */
693
+ const interceptConfig = match.route.o.intercept;
694
+ if (interceptConfig && !options._popstate && !options.revalidate) {
695
+ const currentVarPath = ctx.location().variablePath;
696
+ if (interceptConfig.from.includes(currentVarPath)) {
697
+ try {
698
+ /* Load modules + fetch data for the intercepted target */
699
+ const modules = await loadRouteModules(matchPath, ctx.routeTree, ctx.layouts);
700
+ if (controller.signal.aborted || myVersion !== navigationVersion) return;
701
+
702
+ const search = parseSearchParams(url.searchParams);
703
+ const fetchResult = await fetchNDJSON({
704
+ queryClient: queryClientRef,
705
+ signal: controller.signal,
706
+ url: url.href,
707
+ });
708
+ if (controller.signal.aborted || myVersion !== navigationVersion) return;
709
+
710
+ /* Update matchCache with fetched data */
711
+ const now = Date.now();
712
+ for (const m of fetchResult.matches) {
713
+ ctx.matchCache.set({
714
+ data: m.loaderData,
715
+ error: m.error,
716
+ hasDeferred: m.hasDeferredMarkers,
717
+ invalid: false,
718
+ matchId: m.matchId,
719
+ preloaderContext: m.preloaderContext,
720
+ updatedAt: now,
721
+ });
722
+ }
723
+
724
+ /* Build single ClientMatch for the intercepted page */
725
+ const allModules: LoadedRouteModule[] = [modules.page];
726
+ const clientMatches = buildClientMatches(allModules, search, modules.params);
727
+ const interceptedMatch = clientMatches[0];
728
+
729
+ if (interceptedMatch) {
730
+ /* Freeze current location as background */
731
+ const backgroundLocation = { ...ctx.location() };
732
+
733
+ /* Push state with _intercepted marker */
734
+ if (scrollStore && currentHistoryKey) {
735
+ scrollStore.save(currentHistoryKey, getCurrentScroll());
736
+ }
737
+ incrementHistoryIndex();
738
+ const newState = pushHistoryState(url.pathname, match.params, url.search, {
739
+ hash: url.hash,
740
+ historyIndex: getHistoryIndex(),
741
+ state: { _intercepted: true },
742
+ });
743
+ currentHistoryKey = newState.key;
744
+
745
+ /* Set intercepted signal — background matches stay untouched */
746
+ c.setIntercepted({
747
+ backgroundLocation,
748
+ dismiss: () => {
749
+ if (typeof history !== "undefined") history.back();
750
+ },
751
+ match: interceptedMatch,
752
+ render: interceptConfig.render,
753
+ });
754
+ c.setParams(modules.params);
755
+ c.setSearch(search);
756
+ syncLocale(modules.params);
757
+ }
758
+
759
+ stopNavigation();
760
+ return;
761
+ } catch (error: unknown) {
762
+ if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") return;
763
+ /* Fall through to normal navigation on intercept failure */
764
+ }
765
+ }
766
+ }
767
+
768
+ /* Step 4: Save scroll + update history (skip for popstate — handled in listener) */
769
+ if (!options._popstate) {
770
+ handleHistoryUpdate(options, url, match.params);
771
+ }
772
+
773
+ /* Step 4b: Hash-only change — skip loaders, just update hash + scroll.
774
+ * Compare against ctx.location() since Step 4 already updated window.location.
775
+ * Skip for popstate — browser already updated window.location, so ctx.location()
776
+ * reads the new URL, making the comparison always match (false positive).
777
+ * Skip when revalidate is set — caller explicitly wants loaders to re-run. */
778
+ if (!options._popstate && !options.revalidate) {
779
+ const loc = ctx.location();
780
+ const currentSearch = serializeSearchParams(loc.search);
781
+ if (url.pathname === loc.pathname && url.search === currentSearch) {
782
+ const search = parseSearchParams(url.searchParams);
783
+ batch(() => {
784
+ c.setIntercepted(null);
785
+ c.setParams(match.params);
786
+ c.setSearch(search);
787
+ });
788
+ syncLocale(match.params);
789
+ if (url.hash) {
790
+ const el = typeof document !== "undefined" ? document.getElementById(url.hash.slice(1)) : null;
791
+ if (el) {
792
+ el.scrollIntoView();
793
+ } else {
794
+ scrollToTop();
795
+ }
796
+ }
797
+ stopNavigation();
798
+ return;
799
+ }
800
+ }
801
+
802
+ /* Step 5: Shallow navigation guard — only works for same-route URL changes */
803
+ if (options.shallow) {
804
+ const currentPage = c.matches().at(-1)?.virtualPath;
805
+ const targetPage = match.route.x;
806
+ if (currentPage !== targetPage) {
807
+ warn("nav", `shallow navigation to different route "${targetPage}" ignored`);
808
+ } else {
809
+ let validatedParams: Record<string, string | string[]> = match.params;
810
+ let validatedSearch: SearchParams = parseSearchParams(url.searchParams);
811
+
812
+ try {
813
+ const mod = await match.route
814
+ .p()
815
+ .then((m: { default?: unknown }) => (m.default ?? m) as Record<string, unknown>);
816
+ const ic = mod.inputConfig as
817
+ | {
818
+ params?:
819
+ | ((r: Record<string, string | string[]>) => Record<string, string | string[]>)
820
+ | {
821
+ parse: (r: Record<string, string | string[]>) => Record<string, string | string[]>;
822
+ };
823
+ searchParams?:
824
+ | ((r: URLSearchParams) => Record<string, string>)
825
+ | { parse: (r: URLSearchParams) => Record<string, string> };
826
+ }
827
+ | undefined;
828
+
829
+ if (ic?.params) {
830
+ validatedParams = typeof ic.params === "function" ? ic.params(match.params) : ic.params.parse(match.params);
831
+ }
832
+ if (ic?.searchParams) {
833
+ validatedSearch =
834
+ typeof ic.searchParams === "function"
835
+ ? ic.searchParams(url.searchParams)
836
+ : ic.searchParams.parse(url.searchParams);
837
+ }
838
+ } catch (e: unknown) {
839
+ warn("nav", `shallow validation failed: ${e instanceof Error ? e.message : String(e)}`);
840
+ }
841
+
842
+ batch(() => {
843
+ c.setParams(validatedParams);
844
+ c.setSearch(validatedSearch);
845
+ stopNavigation();
846
+ });
847
+ syncLocale(validatedParams);
848
+ return;
849
+ }
850
+ }
851
+
852
+ try {
853
+ /* Step 6: Load route modules */
854
+ const isNewRoute = !visitedRoutes.has(match.route.x);
855
+ let modules: Awaited<ReturnType<LoadRouteModulesFn>>;
856
+ let fetchResult: Awaited<ReturnType<typeof fetchNDJSON>> | null = null;
857
+
858
+ if (isNewRoute && !options._popstate) {
859
+ /* Parallel fetch for new routes — can't compute stale matchIds without modules */
860
+ const [mods, fetched] = await Promise.all([
861
+ loadRouteModules(matchPath, ctx.routeTree, ctx.layouts),
862
+ fetchNDJSON({ queryClient: queryClientRef, signal: controller.signal, url: url.href }),
863
+ ]);
864
+ modules = mods;
865
+ fetchResult = fetched;
866
+ } else {
867
+ modules = await loadRouteModules(matchPath, ctx.routeTree, ctx.layouts);
868
+ }
869
+
870
+ if (controller.signal.aborted || myVersion !== navigationVersion) return;
871
+
872
+ /* Mark route as visited */
873
+ visitedRoutes.add(match.route.x);
874
+
875
+ const search = parseSearchParams(url.searchParams);
876
+ /*
877
+ * Root layout wraps all providers and persists across navigations —
878
+ * exclude it from client matches so Outlet never re-renders it.
879
+ */
880
+ const nonRootLayouts = modules.layouts.filter((m) => m._type !== "root-layout");
881
+ const allModules: LoadedRouteModule[] = [...nonRootLayouts, modules.page];
882
+
883
+ /* Step 7: Compute match IDs + check staleness (only for known-cached routes) */
884
+ if (!fetchResult) {
885
+ const staleMatchIds: string[] = [];
886
+
887
+ for (const mod of allModules) {
888
+ const deps = mod.effectsConfig?.loaderDeps?.({ search }) ?? [];
889
+ const matchId = computeMatchId({
890
+ loaderDeps: () => deps,
891
+ params: modules.params,
892
+ routeId: mod.virtualPath,
893
+ search,
894
+ });
895
+
896
+ const cc = mod.cache?.client;
897
+ const staleTime =
898
+ cc === false ? 0 : parseMilliseconds(cc?.staleTime ?? ctx?.routerCacheDefaults?.staleTime ?? 0);
899
+ const refetch = mod.effectsConfig?.shouldRefetch?.({
900
+ location: {
901
+ current: {
902
+ hash: ctx.location().hash,
903
+ params: ctx.location().params,
904
+ pathname: ctx.location().pathname,
905
+ search: ctx.location().search,
906
+ },
907
+ next: {
908
+ hash: url.hash,
909
+ params: modules.params,
910
+ pathname: url.pathname,
911
+ search,
912
+ },
913
+ },
914
+ trigger: "navigation",
915
+ });
916
+
917
+ if (options._popstate) {
918
+ /* hasDeferred cache is a shell/marker — restore must refetch
919
+ so Await gets a live promise, not `{ __deferred: true }` */
920
+ if (!ctx.matchCache.isCached(matchId) || ctx.matchCache.get(matchId)?.hasDeferred) {
921
+ staleMatchIds.push(matchId);
922
+ }
923
+ } else if (options.revalidate || refetch || ctx.matchCache.isStale(matchId, staleTime)) {
924
+ staleMatchIds.push(matchId);
925
+ }
926
+ }
927
+
928
+ /* Step 8: Fetch if needed */
929
+ if (staleMatchIds.length > 0) {
930
+ fetchResult = await fetchNDJSON({
931
+ matchIds: staleMatchIds,
932
+ queryClient: queryClientRef,
933
+ signal: controller.signal,
934
+ url: url.href,
935
+ });
936
+ }
937
+ }
938
+
939
+ if (controller.signal.aborted || myVersion !== navigationVersion) return;
940
+
941
+ /* Step 9: Update matchCache (includes headConfig for per-route head management) */
942
+ if (fetchResult) {
943
+ const headByMatchId = new Map<string, HeadConfig>();
944
+ for (const h of fetchResult.perRouteHeads) {
945
+ headByMatchId.set(h.matchId, h.head);
946
+ }
947
+
948
+ /* Build cacheDeferred lookup: matchId → boolean for routes that opt in */
949
+ const cacheDeferredIds = new Set<string>();
950
+ for (const mod of allModules) {
951
+ const clientCache = mod.cache?.client;
952
+ if (clientCache !== false && clientCache !== undefined && clientCache.cacheDeferred) {
953
+ const deps = mod.effectsConfig?.loaderDeps?.({ search }) ?? [];
954
+ const mId = computeMatchId({
955
+ loaderDeps: () => deps,
956
+ params: modules.params,
957
+ routeId: mod.virtualPath,
958
+ search,
959
+ });
960
+ cacheDeferredIds.add(mId);
961
+ }
962
+ }
963
+
964
+ const now = Date.now();
965
+ for (const m of fetchResult.matches) {
966
+ /* x-m skip runs the route with loader stripped → null data.
967
+ Do not clobber a live cache entry that already has data. */
968
+ const existing = ctx.matchCache.get(m.matchId);
969
+ if (m.loaderData == null && !m.error && existing && existing.data != null) {
970
+ const nextHead = headByMatchId.get(m.matchId);
971
+ if (nextHead) {
972
+ existing.headConfig = nextHead;
973
+ ctx.matchCache.set(existing);
974
+ }
975
+ continue;
976
+ }
977
+ ctx.matchCache.set({
978
+ data: m.loaderData,
979
+ error: m.error,
980
+ hasDeferred: m.hasDeferredMarkers,
981
+ headConfig: headByMatchId.get(m.matchId),
982
+ invalid: false,
983
+ matchId: m.matchId,
984
+ preloaderContext: m.preloaderContext,
985
+ updatedAt: now,
986
+ });
987
+
988
+ /* Wire DeferredTracker for cacheDeferred routes */
989
+ if (m.hasDeferredMarkers && deferredTracker && cacheDeferredIds.has(m.matchId)) {
990
+ const collected = collectDeferredPromises(m.loaderData);
991
+ const tracker = deferredTracker;
992
+ for (const d of collected) {
993
+ const gen = tracker.track(m.matchId, d.key, () => {});
994
+ d.promise.then(
995
+ (data) => tracker.resolve(m.matchId, d.key, data, gen),
996
+ (err) => tracker.reject(m.matchId, d.key, err instanceof Error ? err : new Error(String(err)), gen),
997
+ );
998
+ }
999
+ }
1000
+ }
1001
+
1002
+ /* Prune stale deferred entries from previous navigations */
1003
+ if (deferredTracker) {
1004
+ const activeIds = new Set<string>();
1005
+ for (const m of fetchResult.matches) activeIds.add(m.matchId);
1006
+ deferredTracker.prune(activeIds);
1007
+ }
1008
+ }
1009
+
1010
+ /* Failed data fetch with no parsed matches: keep the current match
1011
+ * tree. Rebuilding from cache would drop pipeline errors that were
1012
+ * never written to matchCache (SSR hydrates them via errorName only). */
1013
+ if (fetchResult && fetchResult.success === false && fetchResult.matches.length === 0) {
1014
+ stopNavigation();
1015
+ return;
1016
+ }
1017
+
1018
+ /* Step 10: Build client matches */
1019
+ const clientMatches = buildClientMatches(allModules, search, modules.params);
1020
+
1021
+ /* Step 11: Compute head data before update — used inside VT callback */
1022
+ const freshHeads = fetchResult?.perRouteHeads ?? [];
1023
+ const fetchedHeadIds = new Set<string>();
1024
+ for (const h of freshHeads) fetchedHeadIds.add(h.matchId);
1025
+ const perRouteHeads: PerRouteHead[] = [...freshHeads];
1026
+ for (const mod of allModules) {
1027
+ const deps = mod.effectsConfig?.loaderDeps?.({ search }) ?? [];
1028
+ const matchId = computeMatchId({
1029
+ loaderDeps: () => deps,
1030
+ params: modules.params,
1031
+ routeId: mod.virtualPath,
1032
+ search,
1033
+ });
1034
+ if (!fetchedHeadIds.has(matchId)) {
1035
+ const cached = ctx.matchCache.get(matchId);
1036
+ if (cached?.headConfig) {
1037
+ perRouteHeads.push({ head: cached.headConfig, matchId });
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ /* Step 12: Update state + scroll + head + cleanup.
1043
+ * Everything inside update() so VT captures the complete state transition. */
1044
+ const update = () => {
1045
+ batch(() => {
1046
+ c.setIntercepted(null);
1047
+ c.setNotFound(false);
1048
+ c.setMatches(clientMatches);
1049
+ c.setParams(modules.params);
1050
+ c.setSearch(search);
1051
+ });
1052
+ syncLocale(modules.params);
1053
+
1054
+ /* Popstate scroll restoration — double rAF ensures SolidJS has flushed its
1055
+ * reactive updates and the DOM is ready before we scroll.
1056
+ * Version check prevents stale restore when a new navigation supersedes this one. */
1057
+ if (scrollRestorationEnabled && options._restoreScroll !== undefined) {
1058
+ requestAnimationFrame(() => {
1059
+ requestAnimationFrame(() => {
1060
+ if (myVersion !== navigationVersion) return;
1061
+ if (options._restoreScroll) {
1062
+ restoreScroll(options._restoreScroll, scrollRestorationBehavior);
1063
+ } else {
1064
+ scrollToTop();
1065
+ }
1066
+ });
1067
+ });
1068
+ } else if (options.scroll !== false) {
1069
+ if (url.hash) {
1070
+ const el = typeof document !== "undefined" ? document.getElementById(url.hash.slice(1)) : null;
1071
+ if (el) {
1072
+ el.scrollIntoView();
1073
+ } else {
1074
+ scrollToTop();
1075
+ }
1076
+ } else {
1077
+ scrollToTop();
1078
+ }
1079
+ }
1080
+
1081
+ applyPerRouteHeads(perRouteHeads);
1082
+ };
1083
+
1084
+ /* Step 13: Apply view transition or direct update.
1085
+ * Await ensures navigate() doesn't resolve until update() has run,
1086
+ * so callers see settled state (matches, params, head) after await.
1087
+ * Phase management is handled by applyViewTransition:
1088
+ * - VT path: "transitioning" after updateCallbackDone, "idle" on finished
1089
+ * - No VT: "idle" immediately after update() */
1090
+ const resolvedVT = options.viewTransition ?? defaultViewTransition;
1091
+ await applyViewTransition(resolvedVT, update, options, url, myVersion);
1092
+ } catch (error: unknown) {
1093
+ if (error instanceof RedirectResponse) {
1094
+ if (error.external) {
1095
+ stopNavigation();
1096
+ hardNavigate(error.url);
1097
+ return;
1098
+ }
1099
+ if (redirectCount >= MAX_REDIRECTS) {
1100
+ stopNavigation();
1101
+ throw new Error(
1102
+ `Redirect loop detected after ${redirectCount} redirects. Last target: ${error.url}. Check route redirects for circular references.`,
1103
+ { cause: error },
1104
+ );
1105
+ }
1106
+ return navigate(
1107
+ {
1108
+ replace: error.replace ?? true,
1109
+ to: error.url,
1110
+ },
1111
+ redirectCount + 1,
1112
+ );
1113
+ }
1114
+ if (error instanceof DOMException && error.name === "AbortError") {
1115
+ return;
1116
+ }
1117
+ if (isChunkLoadError(error) && typeof window !== "undefined") {
1118
+ try {
1119
+ const key = "__flare_chunk_reload__";
1120
+ const last = Number(sessionStorage.getItem(key) || 0);
1121
+ if (Date.now() - last > 10_000) {
1122
+ sessionStorage.setItem(key, String(Date.now()));
1123
+ window.location.reload();
1124
+ return;
1125
+ }
1126
+ } catch {
1127
+ /* sessionStorage unavailable — reload once without guard */
1128
+ window.location.reload();
1129
+ return;
1130
+ }
1131
+ /* Already reloaded recently — don't loop, let the error propagate */
1132
+ }
1133
+ stopNavigation();
1134
+ throw error;
1135
+ }
1136
+ }
1137
+
1138
+ const DEFAULT_PREFETCH_STALE_TIME = 30_000;
1139
+
1140
+ export async function prefetch(options: {
1141
+ params?: Record<string, unknown>;
1142
+ search?: Record<string, unknown>;
1143
+ to: string;
1144
+ }): Promise<void> {
1145
+ if (!ctx) return;
1146
+ if (!loadRouteModules) return;
1147
+
1148
+ const resolved = buildUrl({ params: options.params, search: options.search, to: options.to });
1149
+ const url = new URL(resolved, typeof window !== "undefined" ? window.location.href : "http://localhost/");
1150
+
1151
+ /* Resolve route to read per-route prefetchStaleTime, fall back to default */
1152
+ const match = matchRoute(ctx.routeTree, rewritePathname(url.pathname), caseSensitive, toLocaleMatch(localeConfig));
1153
+ if (!match) return;
1154
+
1155
+ const staleTime = parseMilliseconds(
1156
+ match?.route.o.client?.prefetchStaleTime ??
1157
+ ctx?.routerCacheDefaults?.prefetchStaleTime ??
1158
+ DEFAULT_PREFETCH_STALE_TIME,
1159
+ );
1160
+
1161
+ if (!ctx.prefetchCache.shouldPrefetch(url.href, staleTime)) return;
1162
+
1163
+ try {
1164
+ const [fetchResult] = await Promise.all([
1165
+ fetchNDJSON({ prefetch: true, queryClient: queryClientRef, url: url.href }),
1166
+ loadRouteModules(rewritePathname(url.pathname), ctx.routeTree, ctx.layouts),
1167
+ ]);
1168
+
1169
+ ctx.prefetchCache.mark(url.href);
1170
+
1171
+ /* Mark route visited so navigate() checks staleness instead of blind re-fetch */
1172
+ if (match) visitedRoutes.add(match.route.x);
1173
+
1174
+ const headByMatchId = new Map<string, HeadConfig>();
1175
+ for (const h of fetchResult.perRouteHeads) {
1176
+ headByMatchId.set(h.matchId, h.head);
1177
+ }
1178
+ const now = Date.now();
1179
+ for (const m of fetchResult.matches) {
1180
+ ctx.matchCache.set({
1181
+ data: m.loaderData,
1182
+ error: m.error,
1183
+ hasDeferred: m.hasDeferredMarkers,
1184
+ headConfig: headByMatchId.get(m.matchId),
1185
+ invalid: false,
1186
+ matchId: m.matchId,
1187
+ preloaderContext: m.preloaderContext,
1188
+ updatedAt: now,
1189
+ });
1190
+ }
1191
+ } catch {
1192
+ /* Silently discard errors including redirects — prefetch is speculative
1193
+ * (hover/touch). Navigating on redirect would move the user away from
1194
+ * the current page before they actually click the link. */
1195
+ }
1196
+ }
1197
+
1198
+ /* Extracted for testability — jsdom can't spy on window.location.href assignment */
1199
+ export function hardNavigate(href: string): void {
1200
+ if (typeof window !== "undefined") window.location.href = href;
1201
+ }
1202
+
1203
+ export function resetNavigationState(): void {
1204
+ ctx = null;
1205
+ currentController = null;
1206
+ navigationVersion = 0;
1207
+ scrollStore = null;
1208
+ loadRouteModules = null;
1209
+ rewrite = undefined;
1210
+ caseSensitive = false;
1211
+ currentHistoryKey = null;
1212
+ defaultViewTransition = false;
1213
+ notFoundMode = "fuzzy";
1214
+ queryClientRef = undefined;
1215
+ scrollRestorationEnabled = true;
1216
+ scrollRestorationBehavior = "auto";
1217
+ if (deferredTracker) {
1218
+ deferredTracker.clear();
1219
+ deferredTracker = null;
1220
+ }
1221
+ visitedRoutes.clear();
1222
+ activeBlockerFn = null;
1223
+ onBlockedCallback = null;
1224
+ pendingNavigation = null;
1225
+ if (popstateCleanup) {
1226
+ popstateCleanup();
1227
+ popstateCleanup = null;
1228
+ }
1229
+ if (clickCleanup) {
1230
+ clickCleanup();
1231
+ clickCleanup = null;
1232
+ }
1233
+ if (gcIntervalId !== null) {
1234
+ clearInterval(gcIntervalId);
1235
+ gcIntervalId = null;
1236
+ }
1237
+ if (keepaliveIntervalId !== null) {
1238
+ clearInterval(keepaliveIntervalId);
1239
+ keepaliveIntervalId = null;
1240
+ }
1241
+ if (keepaliveVisibilityHandler) {
1242
+ document.removeEventListener("visibilitychange", keepaliveVisibilityHandler);
1243
+ keepaliveVisibilityHandler = null;
1244
+ }
1245
+ }
1246
+
1247
+ /** Set rewrite for SSR — server handler calls this before render, clears after. */
1248
+ export function setRewrite(rw: LocationRewrite | undefined): void {
1249
+ rewrite = rw;
1250
+ }
1251
+
1252
+ /**
1253
+ * Apply output rewrite to transform internal path → browser URL.
1254
+ * Used by Link to generate correct hrefs when rewrites are active.
1255
+ */
1256
+ export function applyRewriteOutput(href: string): string {
1257
+ if (!rewrite) return href;
1258
+ const url = new URL(href, "http://localhost");
1259
+ const rewritten = executeRewriteOutput(rewrite, url);
1260
+ let result = rewritten.pathname;
1261
+ if (rewritten.search) result += rewritten.search;
1262
+ if (rewritten.hash) result += rewritten.hash;
1263
+ return result;
1264
+ }
1265
+
1266
+ export function isExternal(href: string): boolean {
1267
+ if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//")) {
1268
+ if (typeof window === "undefined") return true;
1269
+ try {
1270
+ const url = new URL(href, window.location.origin);
1271
+ return url.origin !== window.location.origin;
1272
+ } catch {
1273
+ return false;
1274
+ }
1275
+ }
1276
+ if (href.startsWith("mailto:") || href.startsWith("tel:")) return true;
1277
+ return false;
1278
+ }