@bquery/bquery 1.5.0 → 1.7.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 (380) hide show
  1. package/README.md +193 -23
  2. package/dist/a11y/announce.d.ts +43 -0
  3. package/dist/a11y/announce.d.ts.map +1 -0
  4. package/dist/a11y/audit.d.ts +42 -0
  5. package/dist/a11y/audit.d.ts.map +1 -0
  6. package/dist/a11y/index.d.ts +53 -0
  7. package/dist/a11y/index.d.ts.map +1 -0
  8. package/dist/a11y/media-preferences.d.ts +77 -0
  9. package/dist/a11y/media-preferences.d.ts.map +1 -0
  10. package/dist/a11y/roving-tab-index.d.ts +38 -0
  11. package/dist/a11y/roving-tab-index.d.ts.map +1 -0
  12. package/dist/a11y/skip-link.d.ts +37 -0
  13. package/dist/a11y/skip-link.d.ts.map +1 -0
  14. package/dist/a11y/trap-focus.d.ts +49 -0
  15. package/dist/a11y/trap-focus.d.ts.map +1 -0
  16. package/dist/a11y/types.d.ts +152 -0
  17. package/dist/a11y/types.d.ts.map +1 -0
  18. package/dist/a11y-C5QOVvRn.js +421 -0
  19. package/dist/a11y-C5QOVvRn.js.map +1 -0
  20. package/dist/a11y.es.mjs +14 -0
  21. package/dist/component/component.d.ts +13 -5
  22. package/dist/component/component.d.ts.map +1 -1
  23. package/dist/component/html.d.ts +40 -3
  24. package/dist/component/html.d.ts.map +1 -1
  25. package/dist/component/index.d.ts +3 -2
  26. package/dist/component/index.d.ts.map +1 -1
  27. package/dist/component/library.d.ts.map +1 -1
  28. package/dist/component/scope.d.ts +138 -0
  29. package/dist/component/scope.d.ts.map +1 -0
  30. package/dist/component/types.d.ts +184 -17
  31. package/dist/component/types.d.ts.map +1 -1
  32. package/dist/component-CuuTijA6.js +684 -0
  33. package/dist/component-CuuTijA6.js.map +1 -0
  34. package/dist/component.es.mjs +10 -6
  35. package/dist/{config-DRmZZno3.js → config-BW35FKuA.js} +4 -4
  36. package/dist/config-BW35FKuA.js.map +1 -0
  37. package/dist/constraints-3lV9yyBw.js +100 -0
  38. package/dist/constraints-3lV9yyBw.js.map +1 -0
  39. package/dist/core/collection.d.ts +48 -0
  40. package/dist/core/collection.d.ts.map +1 -1
  41. package/dist/core/element.d.ts +92 -0
  42. package/dist/core/element.d.ts.map +1 -1
  43. package/dist/core/env.d.ts +18 -0
  44. package/dist/core/env.d.ts.map +1 -0
  45. package/dist/core/index.d.ts +1 -0
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/shared.d.ts +8 -0
  48. package/dist/core/shared.d.ts.map +1 -1
  49. package/dist/core/utils/index.d.ts +52 -41
  50. package/dist/core/utils/index.d.ts.map +1 -1
  51. package/dist/core-Cjl7GUu8.js +717 -0
  52. package/dist/core-Cjl7GUu8.js.map +1 -0
  53. package/dist/{core-DPdbItcq.js → core-DnlyjbF2.js} +1 -1
  54. package/dist/{core-DPdbItcq.js.map → core-DnlyjbF2.js.map} +1 -1
  55. package/dist/core.es.mjs +45 -44
  56. package/dist/custom-directives-7wAShnnd.js +9 -0
  57. package/dist/custom-directives-7wAShnnd.js.map +1 -0
  58. package/dist/devtools/devtools.d.ts +212 -0
  59. package/dist/devtools/devtools.d.ts.map +1 -0
  60. package/dist/devtools/index.d.ts +20 -0
  61. package/dist/devtools/index.d.ts.map +1 -0
  62. package/dist/devtools/types.d.ts +69 -0
  63. package/dist/devtools/types.d.ts.map +1 -0
  64. package/dist/devtools-D2fQLhDN.js +122 -0
  65. package/dist/devtools-D2fQLhDN.js.map +1 -0
  66. package/dist/devtools.es.mjs +19 -0
  67. package/dist/dnd/draggable.d.ts +51 -0
  68. package/dist/dnd/draggable.d.ts.map +1 -0
  69. package/dist/dnd/droppable.d.ts +38 -0
  70. package/dist/dnd/droppable.d.ts.map +1 -0
  71. package/dist/dnd/index.d.ts +47 -0
  72. package/dist/dnd/index.d.ts.map +1 -0
  73. package/dist/dnd/sortable.d.ts +43 -0
  74. package/dist/dnd/sortable.d.ts.map +1 -0
  75. package/dist/dnd/types.d.ts +250 -0
  76. package/dist/dnd/types.d.ts.map +1 -0
  77. package/dist/dnd-B8EgyzaI.js +244 -0
  78. package/dist/dnd-B8EgyzaI.js.map +1 -0
  79. package/dist/dnd.es.mjs +6 -0
  80. package/dist/env-NeVmr4Gf.js +19 -0
  81. package/dist/env-NeVmr4Gf.js.map +1 -0
  82. package/dist/forms/create-form.d.ts +49 -0
  83. package/dist/forms/create-form.d.ts.map +1 -0
  84. package/dist/forms/index.d.ts +39 -0
  85. package/dist/forms/index.d.ts.map +1 -0
  86. package/dist/forms/types.d.ts +139 -0
  87. package/dist/forms/types.d.ts.map +1 -0
  88. package/dist/forms/validators.d.ts +179 -0
  89. package/dist/forms/validators.d.ts.map +1 -0
  90. package/dist/forms-C3yovgH9.js +141 -0
  91. package/dist/forms-C3yovgH9.js.map +1 -0
  92. package/dist/forms.es.mjs +14 -0
  93. package/dist/full.d.ts +37 -9
  94. package/dist/full.d.ts.map +1 -1
  95. package/dist/full.es.mjs +186 -91
  96. package/dist/full.iife.js +47 -31
  97. package/dist/full.iife.js.map +1 -1
  98. package/dist/full.umd.js +47 -31
  99. package/dist/full.umd.js.map +1 -1
  100. package/dist/i18n/formatting.d.ts +40 -0
  101. package/dist/i18n/formatting.d.ts.map +1 -0
  102. package/dist/i18n/i18n.d.ts +48 -0
  103. package/dist/i18n/i18n.d.ts.map +1 -0
  104. package/dist/i18n/index.d.ts +57 -0
  105. package/dist/i18n/index.d.ts.map +1 -0
  106. package/dist/i18n/translate.d.ts +83 -0
  107. package/dist/i18n/translate.d.ts.map +1 -0
  108. package/dist/i18n/types.d.ts +156 -0
  109. package/dist/i18n/types.d.ts.map +1 -0
  110. package/dist/i18n-BnnhTFOS.js +89 -0
  111. package/dist/i18n-BnnhTFOS.js.map +1 -0
  112. package/dist/i18n.es.mjs +6 -0
  113. package/dist/index.d.ts +11 -0
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.es.mjs +233 -138
  116. package/dist/media/battery.d.ts +35 -0
  117. package/dist/media/battery.d.ts.map +1 -0
  118. package/dist/media/breakpoints.d.ts +51 -0
  119. package/dist/media/breakpoints.d.ts.map +1 -0
  120. package/dist/media/clipboard.d.ts +30 -0
  121. package/dist/media/clipboard.d.ts.map +1 -0
  122. package/dist/media/device-sensors.d.ts +54 -0
  123. package/dist/media/device-sensors.d.ts.map +1 -0
  124. package/dist/media/geolocation.d.ts +38 -0
  125. package/dist/media/geolocation.d.ts.map +1 -0
  126. package/dist/media/index.d.ts +42 -0
  127. package/dist/media/index.d.ts.map +1 -0
  128. package/dist/media/media-query.d.ts +36 -0
  129. package/dist/media/media-query.d.ts.map +1 -0
  130. package/dist/media/network.d.ts +35 -0
  131. package/dist/media/network.d.ts.map +1 -0
  132. package/dist/media/types.d.ts +173 -0
  133. package/dist/media/types.d.ts.map +1 -0
  134. package/dist/media/viewport.d.ts +32 -0
  135. package/dist/media/viewport.d.ts.map +1 -0
  136. package/dist/media-Di2Ta22s.js +340 -0
  137. package/dist/media-Di2Ta22s.js.map +1 -0
  138. package/dist/media.es.mjs +12 -0
  139. package/dist/motion/index.d.ts +7 -3
  140. package/dist/motion/index.d.ts.map +1 -1
  141. package/dist/motion/morph.d.ts +27 -0
  142. package/dist/motion/morph.d.ts.map +1 -0
  143. package/dist/motion/parallax.d.ts +30 -0
  144. package/dist/motion/parallax.d.ts.map +1 -0
  145. package/dist/motion/reduced-motion.d.ts +36 -3
  146. package/dist/motion/reduced-motion.d.ts.map +1 -1
  147. package/dist/motion/types.d.ts +58 -0
  148. package/dist/motion/types.d.ts.map +1 -1
  149. package/dist/motion/typewriter.d.ts +31 -0
  150. package/dist/motion/typewriter.d.ts.map +1 -0
  151. package/dist/motion-qPj_TYGv.js +530 -0
  152. package/dist/motion-qPj_TYGv.js.map +1 -0
  153. package/dist/motion.es.mjs +27 -23
  154. package/dist/mount-SM07RUa6.js +403 -0
  155. package/dist/mount-SM07RUa6.js.map +1 -0
  156. package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
  157. package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
  158. package/dist/{platform-B7JhGBc7.js → platform-CPbCprb6.js} +3 -3
  159. package/dist/platform-CPbCprb6.js.map +1 -0
  160. package/dist/platform.es.mjs +2 -2
  161. package/dist/plugin/index.d.ts +22 -0
  162. package/dist/plugin/index.d.ts.map +1 -0
  163. package/dist/plugin/registry.d.ts +108 -0
  164. package/dist/plugin/registry.d.ts.map +1 -0
  165. package/dist/plugin/types.d.ts +110 -0
  166. package/dist/plugin/types.d.ts.map +1 -0
  167. package/dist/plugin-cPoOHFLY.js +64 -0
  168. package/dist/plugin-cPoOHFLY.js.map +1 -0
  169. package/dist/plugin.es.mjs +9 -0
  170. package/dist/reactive/computed.d.ts +7 -0
  171. package/dist/reactive/computed.d.ts.map +1 -1
  172. package/dist/reactive-Cfv0RK6x.js +233 -0
  173. package/dist/reactive-Cfv0RK6x.js.map +1 -0
  174. package/dist/reactive.es.mjs +18 -17
  175. package/dist/registry-CWf368tT.js +26 -0
  176. package/dist/registry-CWf368tT.js.map +1 -0
  177. package/dist/router/bq-link.d.ts +112 -0
  178. package/dist/router/bq-link.d.ts.map +1 -0
  179. package/dist/router/constraints.d.ts +9 -0
  180. package/dist/router/constraints.d.ts.map +1 -0
  181. package/dist/router/index.d.ts +14 -6
  182. package/dist/router/index.d.ts.map +1 -1
  183. package/dist/router/match.d.ts +0 -1
  184. package/dist/router/match.d.ts.map +1 -1
  185. package/dist/router/path-pattern.d.ts +14 -0
  186. package/dist/router/path-pattern.d.ts.map +1 -0
  187. package/dist/router/query.d.ts.map +1 -1
  188. package/dist/router/router.d.ts +3 -1
  189. package/dist/router/router.d.ts.map +1 -1
  190. package/dist/router/types.d.ts +48 -4
  191. package/dist/router/types.d.ts.map +1 -1
  192. package/dist/router/use-route.d.ts +50 -0
  193. package/dist/router/use-route.d.ts.map +1 -0
  194. package/dist/router/utils.d.ts +3 -0
  195. package/dist/router/utils.d.ts.map +1 -1
  196. package/dist/router-BrthaP_z.js +473 -0
  197. package/dist/router-BrthaP_z.js.map +1 -0
  198. package/dist/router.es.mjs +13 -10
  199. package/dist/{sanitize-jyJ2ryE2.js → sanitize-B1V4JswB.js} +95 -83
  200. package/dist/sanitize-B1V4JswB.js.map +1 -0
  201. package/dist/security/index.d.ts +2 -0
  202. package/dist/security/index.d.ts.map +1 -1
  203. package/dist/security/sanitize.d.ts +4 -1
  204. package/dist/security/sanitize.d.ts.map +1 -1
  205. package/dist/security/trusted-html.d.ts +53 -0
  206. package/dist/security/trusted-html.d.ts.map +1 -0
  207. package/dist/security.es.mjs +10 -9
  208. package/dist/ssr/hydrate.d.ts +65 -0
  209. package/dist/ssr/hydrate.d.ts.map +1 -0
  210. package/dist/ssr/index.d.ts +59 -0
  211. package/dist/ssr/index.d.ts.map +1 -0
  212. package/dist/ssr/render.d.ts +62 -0
  213. package/dist/ssr/render.d.ts.map +1 -0
  214. package/dist/ssr/serialize.d.ts +118 -0
  215. package/dist/ssr/serialize.d.ts.map +1 -0
  216. package/dist/ssr/types.d.ts +70 -0
  217. package/dist/ssr/types.d.ts.map +1 -0
  218. package/dist/ssr-B2qd_WBB.js +248 -0
  219. package/dist/ssr-B2qd_WBB.js.map +1 -0
  220. package/dist/ssr.es.mjs +9 -0
  221. package/dist/store/create-store.d.ts.map +1 -1
  222. package/dist/store/define-store.d.ts +1 -1
  223. package/dist/store/define-store.d.ts.map +1 -1
  224. package/dist/store/index.d.ts +1 -1
  225. package/dist/store/index.d.ts.map +1 -1
  226. package/dist/store/mapping.d.ts +1 -1
  227. package/dist/store/mapping.d.ts.map +1 -1
  228. package/dist/store/persisted.d.ts +38 -4
  229. package/dist/store/persisted.d.ts.map +1 -1
  230. package/dist/store/types.d.ts +140 -3
  231. package/dist/store/types.d.ts.map +1 -1
  232. package/dist/store/utils.d.ts +2 -2
  233. package/dist/store/utils.d.ts.map +1 -1
  234. package/dist/store/watch.d.ts +1 -1
  235. package/dist/store/watch.d.ts.map +1 -1
  236. package/dist/store-DWpyH6p5.js +338 -0
  237. package/dist/store-DWpyH6p5.js.map +1 -0
  238. package/dist/store.es.mjs +11 -10
  239. package/dist/storybook/index.d.ts +37 -0
  240. package/dist/storybook/index.d.ts.map +1 -0
  241. package/dist/storybook.es.mjs +151 -0
  242. package/dist/storybook.es.mjs.map +1 -0
  243. package/dist/testing/index.d.ts +23 -0
  244. package/dist/testing/index.d.ts.map +1 -0
  245. package/dist/testing/testing.d.ts +156 -0
  246. package/dist/testing/testing.d.ts.map +1 -0
  247. package/dist/testing/types.d.ts +134 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing-CsqjNUyy.js +224 -0
  250. package/dist/testing-CsqjNUyy.js.map +1 -0
  251. package/dist/testing.es.mjs +9 -0
  252. package/dist/type-guards-Do9DWgNp.js +44 -0
  253. package/dist/type-guards-Do9DWgNp.js.map +1 -0
  254. package/dist/untrack-DJVQQ2WM.js +33 -0
  255. package/dist/untrack-DJVQQ2WM.js.map +1 -0
  256. package/dist/view/custom-directives.d.ts +20 -0
  257. package/dist/view/custom-directives.d.ts.map +1 -0
  258. package/dist/view/evaluate.d.ts.map +1 -1
  259. package/dist/view/process.d.ts.map +1 -1
  260. package/dist/view.es.mjs +11 -10
  261. package/package.json +52 -11
  262. package/src/a11y/announce.ts +131 -0
  263. package/src/a11y/audit.ts +314 -0
  264. package/src/a11y/index.ts +68 -0
  265. package/src/a11y/media-preferences.ts +255 -0
  266. package/src/a11y/roving-tab-index.ts +164 -0
  267. package/src/a11y/skip-link.ts +255 -0
  268. package/src/a11y/trap-focus.ts +184 -0
  269. package/src/a11y/types.ts +183 -0
  270. package/src/component/component.ts +345 -65
  271. package/src/component/html.ts +153 -53
  272. package/src/component/index.ts +12 -2
  273. package/src/component/library.ts +66 -28
  274. package/src/component/scope.ts +212 -0
  275. package/src/component/types.ts +238 -19
  276. package/src/core/collection.ts +707 -628
  277. package/src/core/element.ts +981 -774
  278. package/src/core/env.ts +60 -0
  279. package/src/core/index.ts +49 -48
  280. package/src/core/shared.ts +62 -13
  281. package/src/core/utils/index.ts +148 -83
  282. package/src/devtools/devtools.ts +410 -0
  283. package/src/devtools/index.ts +48 -0
  284. package/src/devtools/types.ts +104 -0
  285. package/src/dnd/draggable.ts +296 -0
  286. package/src/dnd/droppable.ts +228 -0
  287. package/src/dnd/index.ts +62 -0
  288. package/src/dnd/sortable.ts +307 -0
  289. package/src/dnd/types.ts +293 -0
  290. package/src/forms/create-form.ts +278 -0
  291. package/src/forms/index.ts +65 -0
  292. package/src/forms/types.ts +154 -0
  293. package/src/forms/validators.ts +265 -0
  294. package/src/full.ts +260 -3
  295. package/src/i18n/formatting.ts +67 -0
  296. package/src/i18n/i18n.ts +200 -0
  297. package/src/i18n/index.ts +67 -0
  298. package/src/i18n/translate.ts +182 -0
  299. package/src/i18n/types.ts +171 -0
  300. package/src/index.ts +108 -36
  301. package/src/media/battery.ts +116 -0
  302. package/src/media/breakpoints.ts +131 -0
  303. package/src/media/clipboard.ts +80 -0
  304. package/src/media/device-sensors.ts +158 -0
  305. package/src/media/geolocation.ts +119 -0
  306. package/src/media/index.ts +76 -0
  307. package/src/media/media-query.ts +92 -0
  308. package/src/media/network.ts +115 -0
  309. package/src/media/types.ts +177 -0
  310. package/src/media/viewport.ts +84 -0
  311. package/src/motion/index.ts +57 -48
  312. package/src/motion/morph.ts +151 -0
  313. package/src/motion/parallax.ts +120 -0
  314. package/src/motion/reduced-motion.ts +66 -17
  315. package/src/motion/transition.ts +97 -97
  316. package/src/motion/types.ts +63 -0
  317. package/src/motion/typewriter.ts +164 -0
  318. package/src/platform/announcer.ts +208 -208
  319. package/src/platform/config.ts +163 -163
  320. package/src/platform/cookies.ts +165 -165
  321. package/src/platform/index.ts +39 -39
  322. package/src/platform/meta.ts +168 -168
  323. package/src/plugin/index.ts +37 -0
  324. package/src/plugin/registry.ts +269 -0
  325. package/src/plugin/types.ts +137 -0
  326. package/src/reactive/async-data.ts +486 -486
  327. package/src/reactive/computed.ts +130 -92
  328. package/src/reactive/index.ts +37 -37
  329. package/src/reactive/signal.ts +29 -29
  330. package/src/router/bq-link.ts +279 -0
  331. package/src/router/constraints.ts +201 -0
  332. package/src/router/index.ts +49 -41
  333. package/src/router/match.ts +312 -106
  334. package/src/router/path-pattern.ts +52 -0
  335. package/src/router/query.ts +38 -35
  336. package/src/router/router.ts +402 -211
  337. package/src/router/types.ts +139 -93
  338. package/src/router/use-route.ts +68 -0
  339. package/src/router/utils.ts +157 -116
  340. package/src/security/constants.ts +211 -211
  341. package/src/security/index.ts +12 -10
  342. package/src/security/sanitize.ts +6 -2
  343. package/src/security/trusted-html.ts +71 -0
  344. package/src/ssr/hydrate.ts +82 -0
  345. package/src/ssr/index.ts +70 -0
  346. package/src/ssr/render.ts +508 -0
  347. package/src/ssr/serialize.ts +296 -0
  348. package/src/ssr/types.ts +81 -0
  349. package/src/store/create-store.ts +467 -329
  350. package/src/store/define-store.ts +2 -1
  351. package/src/store/index.ts +27 -22
  352. package/src/store/mapping.ts +2 -1
  353. package/src/store/persisted.ts +249 -61
  354. package/src/store/types.ts +247 -94
  355. package/src/store/utils.ts +135 -141
  356. package/src/store/watch.ts +2 -1
  357. package/src/storybook/index.ts +480 -0
  358. package/src/testing/index.ts +42 -0
  359. package/src/testing/testing.ts +593 -0
  360. package/src/testing/types.ts +170 -0
  361. package/src/view/custom-directives.ts +30 -0
  362. package/src/view/evaluate.ts +292 -290
  363. package/src/view/process.ts +108 -92
  364. package/dist/component-CY5MVoYN.js +0 -531
  365. package/dist/component-CY5MVoYN.js.map +0 -1
  366. package/dist/config-DRmZZno3.js.map +0 -1
  367. package/dist/core-CK2Mfpf4.js +0 -648
  368. package/dist/core-CK2Mfpf4.js.map +0 -1
  369. package/dist/motion-C5DRdPnO.js +0 -415
  370. package/dist/motion-C5DRdPnO.js.map +0 -1
  371. package/dist/platform-B7JhGBc7.js.map +0 -1
  372. package/dist/reactive-BDya-ia8.js +0 -253
  373. package/dist/reactive-BDya-ia8.js.map +0 -1
  374. package/dist/router-CijiICxt.js +0 -188
  375. package/dist/router-CijiICxt.js.map +0 -1
  376. package/dist/sanitize-jyJ2ryE2.js.map +0 -1
  377. package/dist/store-CPK9E62U.js +0 -262
  378. package/dist/store-CPK9E62U.js.map +0 -1
  379. package/dist/view-Cdi0g-qo.js +0 -396
  380. package/dist/view-Cdi0g-qo.js.map +0 -1
@@ -1,211 +1,402 @@
1
- /**
2
- * Router creation and lifecycle management.
3
- * @module bquery/router
4
- */
5
-
6
- import { createRoute } from './match';
7
- import { currentRoute, getActiveRouter, routeSignal, setActiveRouter } from './state';
8
- import type { NavigationGuard, Route, Router, RouterOptions } from './types';
9
- import { flattenRoutes } from './utils';
10
-
11
- // ============================================================================
12
- // Router Creation
13
- // ============================================================================
14
-
15
- /**
16
- * Creates and initializes a router instance.
17
- *
18
- * @param options - Router configuration
19
- * @returns The router instance
20
- *
21
- * @example
22
- * ```ts
23
- * import { createRouter } from 'bquery/router';
24
- *
25
- * const router = createRouter({
26
- * routes: [
27
- * { path: '/', component: () => import('./pages/Home') },
28
- * { path: '/about', component: () => import('./pages/About') },
29
- * { path: '/user/:id', component: () => import('./pages/User') },
30
- * { path: '*', component: () => import('./pages/NotFound') },
31
- * ],
32
- * base: '/app',
33
- * });
34
- *
35
- * router.beforeEach((to, from) => {
36
- * if (to.path === '/admin' && !isAuthenticated()) {
37
- * return false; // Cancel navigation
38
- * }
39
- * });
40
- * ```
41
- */
42
- export const createRouter = (options: RouterOptions): Router => {
43
- // Clean up any existing router to prevent guard leakage
44
- const existingRouter = getActiveRouter();
45
- if (existingRouter) {
46
- existingRouter.destroy();
47
- }
48
-
49
- const { routes, base = '', hash: useHash = false } = options;
50
-
51
- // Instance-specific guards and hooks (not shared globally)
52
- const beforeGuards: NavigationGuard[] = [];
53
- const afterHooks: Array<(to: Route, from: Route) => void> = [];
54
-
55
- // Flatten nested routes (base-relative, not including the base path)
56
- const flatRoutes = flattenRoutes(routes);
57
-
58
- /**
59
- * Gets the current path from the URL.
60
- */
61
- const getCurrentPath = (): { pathname: string; search: string; hash: string } => {
62
- if (useHash) {
63
- const hashPath = window.location.hash.slice(1) || '/';
64
- // In hash routing, URL structure is #/path?query#fragment
65
- // Extract hash fragment first (after the second #)
66
- const [pathWithQuery, hashPart = ''] = hashPath.split('#');
67
- // Then extract query from the path
68
- const [pathname, search = ''] = pathWithQuery.split('?');
69
- return {
70
- pathname,
71
- search: search ? `?${search}` : '',
72
- hash: hashPart ? `#${hashPart}` : '',
73
- };
74
- }
75
-
76
- let pathname = window.location.pathname;
77
- if (base && (pathname === base || pathname.startsWith(base + '/'))) {
78
- pathname = pathname.slice(base.length) || '/';
79
- }
80
-
81
- return {
82
- pathname,
83
- search: window.location.search,
84
- hash: window.location.hash,
85
- };
86
- };
87
-
88
- /**
89
- * Updates the route signal with current URL state.
90
- */
91
- const syncRoute = (): void => {
92
- const { pathname, search, hash } = getCurrentPath();
93
- const newRoute = createRoute(pathname, search, hash, flatRoutes);
94
- routeSignal.value = newRoute;
95
- };
96
-
97
- /**
98
- * Performs navigation with guards.
99
- */
100
- const performNavigation = async (
101
- path: string,
102
- method: 'pushState' | 'replaceState'
103
- ): Promise<void> => {
104
- const { pathname, search, hash } = getCurrentPath();
105
- const from = createRoute(pathname, search, hash, flatRoutes);
106
-
107
- // Parse the target path
108
- const url = new URL(path, window.location.origin);
109
- const to = createRoute(url.pathname, url.search, url.hash, flatRoutes);
110
-
111
- // Run beforeEach guards
112
- for (const guard of beforeGuards) {
113
- const result = await guard(to, from);
114
- if (result === false) {
115
- return; // Cancel navigation
116
- }
117
- }
118
-
119
- // Update browser history
120
- const fullPath = useHash ? `#${path}` : `${base}${path}`;
121
- history[method]({}, '', fullPath);
122
-
123
- // Update route signal
124
- syncRoute();
125
-
126
- // Run afterEach hooks
127
- for (const hook of afterHooks) {
128
- hook(routeSignal.value, from);
129
- }
130
- };
131
-
132
- /**
133
- * Handle popstate events (back/forward).
134
- */
135
- const handlePopState = async (): Promise<void> => {
136
- const { pathname, search, hash } = getCurrentPath();
137
- const from = routeSignal.value;
138
- const to = createRoute(pathname, search, hash, flatRoutes);
139
-
140
- // Run beforeEach guards (supports async guards)
141
- for (const guard of beforeGuards) {
142
- const result = await guard(to, from);
143
- if (result === false) {
144
- // Restore previous state with full URL (including query/hash)
145
- const queryString = new URLSearchParams(
146
- Object.entries(from.query).flatMap(([key, value]) =>
147
- Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
148
- )
149
- ).toString();
150
- const search = queryString ? `?${queryString}` : '';
151
- const hash = from.hash ? `#${from.hash}` : '';
152
- const restorePath = useHash
153
- ? `#${from.path}${search}${hash}`
154
- : `${base}${from.path}${search}${hash}`;
155
- history.replaceState({}, '', restorePath);
156
- return;
157
- }
158
- }
159
-
160
- syncRoute();
161
-
162
- for (const hook of afterHooks) {
163
- hook(routeSignal.value, from);
164
- }
165
- };
166
-
167
- // Attach popstate listener
168
- window.addEventListener('popstate', handlePopState);
169
-
170
- // Initialize route
171
- syncRoute();
172
-
173
- const router: Router = {
174
- push: (path: string) => performNavigation(path, 'pushState'),
175
- replace: (path: string) => performNavigation(path, 'replaceState'),
176
- back: () => history.back(),
177
- forward: () => history.forward(),
178
- go: (delta: number) => history.go(delta),
179
-
180
- beforeEach: (guard: NavigationGuard) => {
181
- beforeGuards.push(guard);
182
- return () => {
183
- const index = beforeGuards.indexOf(guard);
184
- if (index > -1) beforeGuards.splice(index, 1);
185
- };
186
- },
187
-
188
- afterEach: (hook: (to: Route, from: Route) => void) => {
189
- afterHooks.push(hook);
190
- return () => {
191
- const index = afterHooks.indexOf(hook);
192
- if (index > -1) afterHooks.splice(index, 1);
193
- };
194
- },
195
-
196
- currentRoute,
197
- routes: flatRoutes,
198
- base,
199
- hash: useHash,
200
-
201
- destroy: () => {
202
- window.removeEventListener('popstate', handlePopState);
203
- beforeGuards.length = 0;
204
- afterHooks.length = 0;
205
- setActiveRouter(null);
206
- },
207
- };
208
-
209
- setActiveRouter(router);
210
- return router;
211
- };
1
+ /**
2
+ * Router creation and lifecycle management.
3
+ * @module bquery/router
4
+ */
5
+
6
+ import { isPrototypePollutionKey } from '../core/utils/object';
7
+ import { createRoute } from './match';
8
+ import { currentRoute, getActiveRouter, routeSignal, setActiveRouter } from './state';
9
+ import type { NavigationGuard, Route, Router, RouterOptions } from './types';
10
+ import { flattenRoutes } from './utils';
11
+
12
+ // ============================================================================
13
+ // Router Creation
14
+ // ============================================================================
15
+
16
+ const MAX_SCROLL_POSITION_ENTRIES = 100;
17
+
18
+ const sanitizeHistoryState = (state: Record<string, unknown>): Record<string, unknown> => {
19
+ const sanitized: Record<string, unknown> = {};
20
+
21
+ for (const [key, value] of Object.entries(state)) {
22
+ if (isPrototypePollutionKey(key)) continue;
23
+ sanitized[key] = value;
24
+ }
25
+
26
+ return sanitized;
27
+ };
28
+
29
+ /**
30
+ * Creates and initializes a router instance.
31
+ *
32
+ * @param options - Router configuration
33
+ * @returns The router instance
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { createRouter } from 'bquery/router';
38
+ *
39
+ * const router = createRouter({
40
+ * routes: [
41
+ * { path: '/', component: () => import('./pages/Home') },
42
+ * { path: '/about', component: () => import('./pages/About') },
43
+ * { path: '/user/:id(\\d+)', component: () => import('./pages/User') },
44
+ * { path: '/old-page', redirectTo: '/new-page' },
45
+ * { path: '*', component: () => import('./pages/NotFound') },
46
+ * ],
47
+ * base: '/app',
48
+ * scrollRestoration: true,
49
+ * });
50
+ *
51
+ * router.beforeEach((to, from) => {
52
+ * if (to.path === '/admin' && !isAuthenticated()) {
53
+ * return false; // Cancel navigation
54
+ * }
55
+ * });
56
+ * ```
57
+ */
58
+ export const createRouter = (options: RouterOptions): Router => {
59
+ // Clean up any existing router to prevent guard leakage
60
+ const existingRouter = getActiveRouter();
61
+ if (existingRouter) {
62
+ existingRouter.destroy();
63
+ }
64
+
65
+ const { routes, base = '', hash: useHash = false, scrollRestoration = false } = options;
66
+
67
+ // Instance-specific guards and hooks (not shared globally)
68
+ const beforeGuards: NavigationGuard[] = [];
69
+ const afterHooks: Array<(to: Route, from: Route) => void> = [];
70
+
71
+ // Flatten nested routes (base-relative, not including the base path)
72
+ const flatRoutes = flattenRoutes(routes);
73
+
74
+ // Scroll position storage keyed by history state id
75
+ const scrollPositions = new Map<string, { x: number; y: number }>();
76
+ let currentScrollKey = '0';
77
+ let scrollKeyCounter = 0;
78
+ let previousScrollRestoration: History['scrollRestoration'] | null = null;
79
+
80
+ // Enable manual scroll restoration if scrollRestoration is configured
81
+ if (scrollRestoration && typeof history !== 'undefined' && 'scrollRestoration' in history) {
82
+ previousScrollRestoration = history.scrollRestoration;
83
+ if (history.scrollRestoration !== 'manual') {
84
+ history.scrollRestoration = 'manual';
85
+ }
86
+
87
+ const state =
88
+ history.state && typeof history.state === 'object'
89
+ ? (history.state as Record<string, unknown>)
90
+ : {};
91
+
92
+ if (typeof state.__bqScrollKey !== 'string') {
93
+ const currentUrl = useHash
94
+ ? window.location.hash || '#/'
95
+ : `${window.location.pathname}${window.location.search}${window.location.hash}`;
96
+ history.replaceState({ ...state, __bqScrollKey: currentScrollKey }, '', currentUrl);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Generates a unique key for the current history entry.
102
+ * @internal
103
+ */
104
+ const getScrollKey = (): string => {
105
+ return (history.state && history.state.__bqScrollKey) || currentScrollKey;
106
+ };
107
+
108
+ /**
109
+ * Generates a unique key for a new history entry.
110
+ * @internal
111
+ */
112
+ const createScrollKey = (): string => `${Date.now()}-${scrollKeyCounter++}`;
113
+
114
+ /**
115
+ * Saves current scroll position for the current history entry.
116
+ * @internal
117
+ */
118
+ const saveScrollPosition = (key = getScrollKey()): void => {
119
+ if (!scrollRestoration) return;
120
+ if (scrollPositions.has(key)) {
121
+ // Refresh the insertion order so pruning behaves like an LRU cache.
122
+ scrollPositions.delete(key);
123
+ }
124
+ scrollPositions.set(key, { x: window.scrollX, y: window.scrollY });
125
+ while (scrollPositions.size > MAX_SCROLL_POSITION_ENTRIES) {
126
+ const oldestKey = scrollPositions.keys().next().value as string | undefined;
127
+ if (oldestKey === undefined) {
128
+ break;
129
+ }
130
+ scrollPositions.delete(oldestKey);
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Restores scroll position for the current history entry.
136
+ * @internal
137
+ */
138
+ const restoreScrollPosition = (key = getScrollKey()): void => {
139
+ if (!scrollRestoration) return;
140
+ const pos = scrollPositions.get(key);
141
+ if (pos) {
142
+ window.scrollTo(pos.x, pos.y);
143
+ } else {
144
+ window.scrollTo(0, 0);
145
+ }
146
+ };
147
+
148
+ /**
149
+ * Builds history state for canceled navigations without dropping
150
+ * the scroll restoration key for the current entry.
151
+ * @internal
152
+ */
153
+ const getRestoreHistoryState = (): Record<string, unknown> => {
154
+ const state =
155
+ history.state && typeof history.state === 'object'
156
+ ? { ...(history.state as Record<string, unknown>) }
157
+ : {};
158
+
159
+ if (scrollRestoration) {
160
+ state.__bqScrollKey = currentScrollKey;
161
+ }
162
+
163
+ return state;
164
+ };
165
+
166
+ /**
167
+ * Gets the current path from the URL.
168
+ */
169
+ const getCurrentPath = (): { pathname: string; search: string; hash: string } => {
170
+ if (useHash) {
171
+ const hashPath = window.location.hash.slice(1) || '/';
172
+ // In hash routing, URL structure is #/path?query#fragment
173
+ // Extract hash fragment first (after the second #)
174
+ const [pathWithQuery, hashPart = ''] = hashPath.split('#');
175
+ // Then extract query from the path
176
+ const [pathname, search = ''] = pathWithQuery.split('?');
177
+ return {
178
+ pathname,
179
+ search: search ? `?${search}` : '',
180
+ hash: hashPart ? `#${hashPart}` : '',
181
+ };
182
+ }
183
+
184
+ let pathname = window.location.pathname;
185
+ if (base && (pathname === base || pathname.startsWith(base + '/'))) {
186
+ pathname = pathname.slice(base.length) || '/';
187
+ }
188
+
189
+ return {
190
+ pathname,
191
+ search: window.location.search,
192
+ hash: window.location.hash,
193
+ };
194
+ };
195
+
196
+ /**
197
+ * Updates the route signal with current URL state.
198
+ */
199
+ const syncRoute = (): void => {
200
+ const { pathname, search, hash } = getCurrentPath();
201
+ const newRoute = createRoute(pathname, search, hash, flatRoutes);
202
+ routeSignal.value = newRoute;
203
+ };
204
+
205
+ /**
206
+ * Performs navigation with guards.
207
+ */
208
+ const performNavigation = async (
209
+ path: string,
210
+ method: 'pushState' | 'replaceState',
211
+ visitedPaths: Set<string> = new Set()
212
+ ): Promise<void> => {
213
+ const { pathname, search, hash } = getCurrentPath();
214
+ const from = createRoute(pathname, search, hash, flatRoutes);
215
+
216
+ // Parse the target path
217
+ const url = new URL(path, window.location.origin);
218
+ const resolvedPath = `${url.pathname}${url.search}${url.hash}`;
219
+ if (visitedPaths.has(resolvedPath)) {
220
+ throw new Error(`bQuery router: redirect loop detected for path "${resolvedPath}"`);
221
+ }
222
+ visitedPaths.add(resolvedPath);
223
+ const to = createRoute(url.pathname, url.search, url.hash, flatRoutes);
224
+
225
+ // Check for redirectTo on the matched route
226
+ if (to.matched?.redirectTo) {
227
+ // Navigate to the redirect target instead
228
+ await performNavigation(to.matched.redirectTo, method, visitedPaths);
229
+ return;
230
+ }
231
+
232
+ // Run route-level beforeEnter guard
233
+ if (to.matched?.beforeEnter) {
234
+ const result = await to.matched.beforeEnter(to, from);
235
+ if (result === false) {
236
+ return; // Cancel navigation
237
+ }
238
+ }
239
+
240
+ // Run beforeEach guards
241
+ for (const guard of beforeGuards) {
242
+ const result = await guard(to, from);
243
+ if (result === false) {
244
+ return; // Cancel navigation
245
+ }
246
+ }
247
+
248
+ // Save scroll position before navigation
249
+ saveScrollPosition();
250
+
251
+ // Update browser history
252
+ const existingScrollKey = scrollRestoration ? getScrollKey() : undefined;
253
+ const scrollKey =
254
+ method === 'replaceState' && existingScrollKey ? existingScrollKey : createScrollKey();
255
+ const fullPath = useHash ? `#${path}` : `${base}${path}`;
256
+ const baseState =
257
+ scrollRestoration && history.state && typeof history.state === 'object'
258
+ ? sanitizeHistoryState(history.state as Record<string, unknown>)
259
+ : {};
260
+ const state = scrollRestoration ? { ...baseState, __bqScrollKey: scrollKey } : {};
261
+ history[method](state, '', fullPath);
262
+ currentScrollKey = scrollKey;
263
+
264
+ // Update route signal
265
+ syncRoute();
266
+
267
+ // Scroll to top on push navigation
268
+ if (scrollRestoration && method === 'pushState') {
269
+ window.scrollTo(0, 0);
270
+ }
271
+
272
+ // Run afterEach hooks
273
+ for (const hook of afterHooks) {
274
+ hook(routeSignal.value, from);
275
+ }
276
+ };
277
+
278
+ /**
279
+ * Handle popstate events (back/forward).
280
+ */
281
+ const handlePopState = async (event: PopStateEvent): Promise<void> => {
282
+ const { pathname, search, hash } = getCurrentPath();
283
+ const from = routeSignal.value;
284
+ const to = createRoute(pathname, search, hash, flatRoutes);
285
+
286
+ // Check for redirectTo on the matched route
287
+ if (to.matched?.redirectTo) {
288
+ await performNavigation(to.matched.redirectTo, 'replaceState');
289
+ return;
290
+ }
291
+
292
+ // Run route-level beforeEnter guard
293
+ if (to.matched?.beforeEnter) {
294
+ const result = await to.matched.beforeEnter(to, from);
295
+ if (result === false) {
296
+ // Restore previous state with full URL (including query/hash)
297
+ const queryString = new URLSearchParams(
298
+ Object.entries(from.query).flatMap(([key, value]) =>
299
+ Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
300
+ )
301
+ ).toString();
302
+ const searchStr = queryString ? `?${queryString}` : '';
303
+ const hashStr = from.hash ? `#${from.hash}` : '';
304
+ const restorePath = useHash
305
+ ? `#${from.path}${searchStr}${hashStr}`
306
+ : `${base}${from.path}${searchStr}${hashStr}`;
307
+ history.replaceState(getRestoreHistoryState(), '', restorePath);
308
+ return;
309
+ }
310
+ }
311
+
312
+ // Run beforeEach guards (supports async guards)
313
+ for (const guard of beforeGuards) {
314
+ const result = await guard(to, from);
315
+ if (result === false) {
316
+ // Restore previous state with full URL (including query/hash)
317
+ const queryString = new URLSearchParams(
318
+ Object.entries(from.query).flatMap(([key, value]) =>
319
+ Array.isArray(value) ? value.map((v) => [key, v]) : [[key, value]]
320
+ )
321
+ ).toString();
322
+ const search = queryString ? `?${queryString}` : '';
323
+ const hash = from.hash ? `#${from.hash}` : '';
324
+ const restorePath = useHash
325
+ ? `#${from.path}${search}${hash}`
326
+ : `${base}${from.path}${search}${hash}`;
327
+ history.replaceState(getRestoreHistoryState(), '', restorePath);
328
+ return;
329
+ }
330
+ }
331
+
332
+ // Save scroll position of the page we're leaving
333
+ saveScrollPosition(currentScrollKey);
334
+
335
+ // Update scroll key from history state
336
+ currentScrollKey =
337
+ (event.state as { __bqScrollKey?: string } | null)?.__bqScrollKey ?? getScrollKey();
338
+
339
+ syncRoute();
340
+
341
+ // Restore scroll position for the entry we're navigating to
342
+ restoreScrollPosition(currentScrollKey);
343
+
344
+ for (const hook of afterHooks) {
345
+ hook(routeSignal.value, from);
346
+ }
347
+ };
348
+
349
+ // Attach popstate listener
350
+ window.addEventListener('popstate', handlePopState);
351
+
352
+ // Initialize route
353
+ syncRoute();
354
+
355
+ const router: Router = {
356
+ push: (path: string) => performNavigation(path, 'pushState'),
357
+ replace: (path: string) => performNavigation(path, 'replaceState'),
358
+ back: () => history.back(),
359
+ forward: () => history.forward(),
360
+ go: (delta: number) => history.go(delta),
361
+
362
+ beforeEach: (guard: NavigationGuard) => {
363
+ beforeGuards.push(guard);
364
+ return () => {
365
+ const index = beforeGuards.indexOf(guard);
366
+ if (index > -1) beforeGuards.splice(index, 1);
367
+ };
368
+ },
369
+
370
+ afterEach: (hook: (to: Route, from: Route) => void) => {
371
+ afterHooks.push(hook);
372
+ return () => {
373
+ const index = afterHooks.indexOf(hook);
374
+ if (index > -1) afterHooks.splice(index, 1);
375
+ };
376
+ },
377
+
378
+ currentRoute,
379
+ routes: flatRoutes,
380
+ base,
381
+ hash: useHash,
382
+
383
+ destroy: () => {
384
+ window.removeEventListener('popstate', handlePopState);
385
+ beforeGuards.length = 0;
386
+ afterHooks.length = 0;
387
+ scrollPositions.clear();
388
+ // Restore the previous scroll restoration mode on destroy
389
+ if (
390
+ previousScrollRestoration !== null &&
391
+ typeof history !== 'undefined' &&
392
+ 'scrollRestoration' in history
393
+ ) {
394
+ history.scrollRestoration = previousScrollRestoration;
395
+ }
396
+ setActiveRouter(null);
397
+ },
398
+ };
399
+
400
+ setActiveRouter(router);
401
+ return router;
402
+ };