@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
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SSR / Pre-rendering module for bQuery.js.
3
+ *
4
+ * Provides server-side rendering, hydration, and store state serialization
5
+ * utilities for bQuery applications. Enables rendering bQuery templates
6
+ * to HTML strings on the server, serializing store state for client pickup,
7
+ * and hydrating the pre-rendered DOM on the client.
8
+ *
9
+ * ## Features
10
+ *
11
+ * - **`renderToString(template, data)`** — Server-side render a bQuery
12
+ * template to an `SSRResult` containing an `html` string with directive evaluation.
13
+ * - **`hydrateMount(selector, context, { hydrate: true })`** — Reuse
14
+ * existing server-rendered DOM and attach reactive bindings.
15
+ * - **`serializeStoreState(options?)`** — Serialize store state into a
16
+ * `<script>` tag for client-side pickup.
17
+ * - **`deserializeStoreState()`** — Read serialized state on the client.
18
+ * - **`hydrateStore(id, state)` / `hydrateStores(stateMap)`** — Apply
19
+ * server state to client stores.
20
+ *
21
+ * ## Usage
22
+ *
23
+ * ### Server
24
+ * ```ts
25
+ * import { renderToString, serializeStoreState } from '@bquery/bquery/ssr';
26
+ *
27
+ * const { html } = renderToString(
28
+ * '<div id="app"><h1 bq-text="title"></h1></div>',
29
+ * { title: 'Welcome' }
30
+ * );
31
+ *
32
+ * const { scriptTag } = serializeStoreState();
33
+ *
34
+ * // Send to client: html + scriptTag
35
+ * ```
36
+ *
37
+ * ### Client
38
+ * ```ts
39
+ * import { hydrateMount, deserializeStoreState, hydrateStores } from '@bquery/bquery/ssr';
40
+ * import { signal } from '@bquery/bquery/reactive';
41
+ *
42
+ * // Restore store state from SSR
43
+ * const ssrState = deserializeStoreState();
44
+ * hydrateStores(ssrState);
45
+ *
46
+ * // Hydrate the DOM with reactive bindings
47
+ * const title = signal('Welcome');
48
+ * hydrateMount('#app', { title }, { hydrate: true });
49
+ * ```
50
+ *
51
+ * @module bquery/ssr
52
+ */
53
+
54
+ export { hydrateMount } from './hydrate';
55
+ export type { HydrateMountOptions } from './hydrate';
56
+ export { renderToString } from './render';
57
+ export {
58
+ deserializeStoreState,
59
+ hydrateStore,
60
+ hydrateStores,
61
+ serializeStoreState,
62
+ } from './serialize';
63
+ export type { SerializeResult } from './serialize';
64
+ export type {
65
+ DeserializedStoreState,
66
+ HydrationOptions,
67
+ RenderOptions,
68
+ SSRResult,
69
+ SerializeOptions,
70
+ } from './types';
@@ -0,0 +1,508 @@
1
+ /**
2
+ * SSR rendering utilities.
3
+ *
4
+ * Server-side renders bQuery templates to HTML strings by evaluating
5
+ * directive attributes against a plain data context. Uses a lightweight
6
+ * DOM implementation to process templates without a browser.
7
+ *
8
+ * @module bquery/ssr
9
+ */
10
+
11
+ import { isComputed, isSignal, type Signal } from '../reactive/index';
12
+ import { DANGEROUS_PROTOCOLS } from '../security/constants';
13
+ import { sanitizeHtml } from '../security/sanitize';
14
+ import type { BindingContext } from '../view/types';
15
+ import type { RenderOptions, SSRResult } from './types';
16
+ import { serializeStoreState } from './serialize';
17
+
18
+ const VOID_ELEMENTS = new Set([
19
+ 'area',
20
+ 'base',
21
+ 'br',
22
+ 'col',
23
+ 'embed',
24
+ 'hr',
25
+ 'img',
26
+ 'input',
27
+ 'link',
28
+ 'meta',
29
+ 'param',
30
+ 'source',
31
+ 'track',
32
+ 'wbr',
33
+ ]);
34
+
35
+ const escapeHtmlText = (value: string): string =>
36
+ value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
37
+
38
+ const escapeHtmlAttribute = (value: string): string =>
39
+ escapeHtmlText(value).replace(/"/g, '&quot;');
40
+
41
+ const isUnsafeUrlAttribute = (name: string): boolean => {
42
+ const normalized = name.toLowerCase();
43
+ return (
44
+ normalized === 'href' ||
45
+ normalized === 'src' ||
46
+ normalized === 'xlink:href' ||
47
+ normalized === 'formaction' ||
48
+ normalized === 'action' ||
49
+ normalized === 'poster' ||
50
+ normalized === 'background' ||
51
+ normalized === 'cite' ||
52
+ normalized === 'data'
53
+ );
54
+ };
55
+
56
+ const sanitizeUrlForProtocolCheck = (value: string): string =>
57
+ value
58
+ .trim()
59
+ .replace(/[\u0000-\u001F\u007F]+/g, '')
60
+ .replace(/[\u200B-\u200D\uFEFF\u2028\u2029]+/g, '')
61
+ .replace(/\\u[\da-fA-F]{4}/g, '')
62
+ .replace(/\s+/g, '')
63
+ .toLowerCase();
64
+
65
+ const isUnsafeUrlValue = (value: string): boolean => {
66
+ const normalized = sanitizeUrlForProtocolCheck(value);
67
+ return DANGEROUS_PROTOCOLS.some((protocol) => normalized.startsWith(protocol));
68
+ };
69
+
70
+ const serializeSSRNode = (node: Node): string => {
71
+ if (node.nodeType === Node.TEXT_NODE) {
72
+ return escapeHtmlText(node.textContent ?? '');
73
+ }
74
+
75
+ if (node.nodeType !== Node.ELEMENT_NODE) {
76
+ return '';
77
+ }
78
+
79
+ const el = node as Element;
80
+ const tagName = el.tagName.toLowerCase();
81
+
82
+ if (tagName === 'script') {
83
+ return '';
84
+ }
85
+
86
+ let attrs = '';
87
+ for (const attr of el.attributes) {
88
+ const attrName = attr.name.toLowerCase();
89
+ if (attrName.startsWith('on')) {
90
+ continue;
91
+ }
92
+ if (isUnsafeUrlAttribute(attrName) && isUnsafeUrlValue(attr.value)) {
93
+ continue;
94
+ }
95
+ attrs += ` ${attr.name}="${escapeHtmlAttribute(attr.value)}"`;
96
+ }
97
+
98
+ if (VOID_ELEMENTS.has(tagName)) {
99
+ return `<${tagName}${attrs}>`;
100
+ }
101
+
102
+ let childrenHtml = '';
103
+ for (const child of el.childNodes) {
104
+ childrenHtml += serializeSSRNode(child);
105
+ }
106
+
107
+ return `<${tagName}${attrs}>${childrenHtml}</${tagName}>`;
108
+ };
109
+
110
+ /**
111
+ * Unwraps a value — if it's a signal/computed, returns `.value`, otherwise returns as-is.
112
+ * @internal
113
+ */
114
+ const unwrap = (value: unknown): unknown => {
115
+ if (isSignal(value) || isComputed(value)) {
116
+ return (value as Signal<unknown>).value;
117
+ }
118
+ return value;
119
+ };
120
+
121
+ /**
122
+ * Evaluates a simple expression against a context.
123
+ * Supports dot-notation property access, negation, ternary, and basic comparisons.
124
+ * Unlike the view module's `evaluate()`, this does NOT use `new Function()` —
125
+ * it uses a safe subset for SSR to avoid `unsafe-eval` in server environments.
126
+ *
127
+ * Falls back to `new Function()` for complex expressions.
128
+ *
129
+ * @internal
130
+ */
131
+ const evaluateSSR = <T = unknown>(expression: string, context: BindingContext): T => {
132
+ const trimmed = expression.trim();
133
+
134
+ // Handle negation: !expr
135
+ if (trimmed.startsWith('!')) {
136
+ return !evaluateSSR(trimmed.slice(1).trim(), context) as T;
137
+ }
138
+
139
+ // Handle string literals
140
+ if (
141
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
142
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
143
+ ) {
144
+ return trimmed.slice(1, -1) as T;
145
+ }
146
+
147
+ // Handle numeric literals
148
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
149
+ return Number(trimmed) as T;
150
+ }
151
+
152
+ // Handle boolean literals
153
+ if (trimmed === 'true') return true as T;
154
+ if (trimmed === 'false') return false as T;
155
+ if (trimmed === 'null') return null as T;
156
+ if (trimmed === 'undefined') return undefined as T;
157
+
158
+ // Handle dot-notation property access: a.b.c
159
+ if (/^[\w$]+(?:\.[\w$]+)*$/.test(trimmed)) {
160
+ const parts = trimmed.split('.');
161
+ let current: unknown = context;
162
+ for (const part of parts) {
163
+ if (current == null) return undefined as T;
164
+ // First level: unwrap signals
165
+ if (current === context) {
166
+ current = unwrap((current as Record<string, unknown>)[part]);
167
+ } else {
168
+ current = (current as Record<string, unknown>)[part];
169
+ }
170
+ }
171
+ return current as T;
172
+ }
173
+
174
+ // For complex expressions, fall back to Function-based evaluation
175
+ try {
176
+ const keys = Object.keys(context);
177
+ const values = keys.map((k) => unwrap(context[k]));
178
+ const fn = new Function(...keys, `return (${trimmed});`);
179
+ return fn(...values) as T;
180
+ } catch {
181
+ return undefined as T;
182
+ }
183
+ };
184
+
185
+ /**
186
+ * Parses a `bq-for` expression like `item in items` or `(item, index) in items`.
187
+ * @internal
188
+ */
189
+ const parseForExpression = (
190
+ expression: string
191
+ ): { itemName: string; indexName?: string; listExpr: string } | null => {
192
+ const match = expression.match(/^\(?(\w+)(?:\s*,\s*(\w+))?\)?\s+in\s+(\S.*)$/);
193
+ if (!match) return null;
194
+ return {
195
+ itemName: match[1],
196
+ indexName: match[2] || undefined,
197
+ listExpr: match[3].trim(),
198
+ };
199
+ };
200
+
201
+ /**
202
+ * Processes an element's SSR directives, modifying it in place.
203
+ * Returns `false` if the element should be removed from output (bq-if = false).
204
+ * @internal
205
+ */
206
+ const processSSRElement = (
207
+ el: Element,
208
+ context: BindingContext,
209
+ prefix: string,
210
+ doc: Document
211
+ ): boolean => {
212
+ // Handle bq-if: remove element if condition is falsy
213
+ const ifExpr = el.getAttribute(`${prefix}-if`);
214
+ if (ifExpr !== null) {
215
+ const condition = evaluateSSR<boolean>(ifExpr, context);
216
+ if (!condition) {
217
+ return false; // Signal to remove this element
218
+ }
219
+ }
220
+
221
+ // Handle bq-show: set display:none if falsy
222
+ const showExpr = el.getAttribute(`${prefix}-show`);
223
+ if (showExpr !== null) {
224
+ const condition = evaluateSSR<boolean>(showExpr, context);
225
+ if (!condition) {
226
+ const htmlEl = el as unknown as { style?: { display?: string } };
227
+ if (htmlEl.style) {
228
+ htmlEl.style.display = 'none';
229
+ } else {
230
+ el.setAttribute('style', 'display: none;');
231
+ }
232
+ }
233
+ }
234
+
235
+ // Handle bq-text: set text content
236
+ const textExpr = el.getAttribute(`${prefix}-text`);
237
+ if (textExpr !== null) {
238
+ const value = evaluateSSR(textExpr, context);
239
+ el.textContent = String(value ?? '');
240
+ }
241
+
242
+ // Handle bq-html: sanitize to match client-side default behavior
243
+ const htmlExpr = el.getAttribute(`${prefix}-html`);
244
+ if (htmlExpr !== null) {
245
+ const value = evaluateSSR(htmlExpr, context);
246
+ el.innerHTML = String(sanitizeHtml(String(value ?? '')));
247
+ }
248
+
249
+ // Handle bq-class: add classes
250
+ const classExpr = el.getAttribute(`${prefix}-class`);
251
+ if (classExpr !== null) {
252
+ const trimmedClass = classExpr.trim();
253
+ if (trimmedClass.startsWith('{')) {
254
+ // Object syntax: { active: isActive, disabled: !enabled }
255
+ const inner = trimmedClass.slice(1, -1).trim();
256
+ const pairs = inner.split(',');
257
+ for (const pair of pairs) {
258
+ const colonIdx = pair.indexOf(':');
259
+ if (colonIdx > -1) {
260
+ const className = pair
261
+ .slice(0, colonIdx)
262
+ .trim()
263
+ .replace(/^['"]|['"]$/g, '');
264
+ const condExpr = pair.slice(colonIdx + 1).trim();
265
+ const condition = evaluateSSR<boolean>(condExpr, context);
266
+ if (condition) {
267
+ el.classList.add(className);
268
+ }
269
+ }
270
+ }
271
+ } else {
272
+ const result = evaluateSSR<string | string[]>(classExpr, context);
273
+ if (typeof result === 'string') {
274
+ result
275
+ .split(/\s+/)
276
+ .filter(Boolean)
277
+ .forEach((cls) => el.classList.add(cls));
278
+ } else if (Array.isArray(result)) {
279
+ result.filter(Boolean).forEach((cls) => el.classList.add(cls));
280
+ }
281
+ }
282
+ }
283
+
284
+ // Handle bq-style: set inline styles
285
+ const styleExpr = el.getAttribute(`${prefix}-style`);
286
+ if (styleExpr !== null) {
287
+ const result = evaluateSSR<Record<string, string>>(styleExpr, context);
288
+ if (result && typeof result === 'object') {
289
+ const htmlEl = el as HTMLElement;
290
+ for (const [prop, val] of Object.entries(result)) {
291
+ // Convert camelCase to kebab-case
292
+ const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
293
+ htmlEl.style.setProperty(cssProp, String(val));
294
+ }
295
+ }
296
+ }
297
+
298
+ // Handle bq-bind:attr — set arbitrary attributes
299
+ const attrs = Array.from(el.attributes);
300
+ for (const attr of attrs) {
301
+ if (attr.name.startsWith(`${prefix}-bind:`)) {
302
+ const attrName = attr.name.slice(`${prefix}-bind:`.length);
303
+ const value = evaluateSSR(attr.value, context);
304
+ if (value === false || value === null || value === undefined) {
305
+ el.removeAttribute(attrName);
306
+ } else if (value === true) {
307
+ el.setAttribute(attrName, '');
308
+ } else {
309
+ el.setAttribute(attrName, String(value));
310
+ }
311
+ }
312
+ }
313
+
314
+ // Handle bq-for: list rendering
315
+ const forExpr = el.getAttribute(`${prefix}-for`);
316
+ if (forExpr !== null) {
317
+ const parsed = parseForExpression(forExpr);
318
+ if (parsed) {
319
+ const list = evaluateSSR<unknown[]>(parsed.listExpr, context);
320
+ if (Array.isArray(list) && el.parentNode) {
321
+ const parent = el.parentNode;
322
+ for (let i = 0; i < list.length; i++) {
323
+ const item = list[i];
324
+ const clone = el.cloneNode(true) as Element;
325
+
326
+ // Remove the bq-for attribute from clones
327
+ clone.removeAttribute(`${prefix}-for`);
328
+ clone.removeAttribute(':key');
329
+ clone.removeAttribute(`${prefix}-key`);
330
+
331
+ // Create item context
332
+ const itemContext: BindingContext = {
333
+ ...context,
334
+ [parsed.itemName]: item,
335
+ };
336
+ if (parsed.indexName) {
337
+ itemContext[parsed.indexName] = i;
338
+ }
339
+
340
+ // Recursively process the clone
341
+ processSSRElement(clone, itemContext, prefix, doc);
342
+ processSSRChildren(clone, itemContext, prefix, doc);
343
+
344
+ parent.insertBefore(clone, el);
345
+ }
346
+
347
+ // Remove the original template element
348
+ parent.removeChild(el);
349
+ return true; // Already handled children
350
+ }
351
+ }
352
+ }
353
+
354
+ return true;
355
+ };
356
+
357
+ /**
358
+ * Recursively processes children of an element for SSR.
359
+ * @internal
360
+ */
361
+ const processSSRChildren = (
362
+ parent: Element,
363
+ context: BindingContext,
364
+ prefix: string,
365
+ doc: Document
366
+ ): void => {
367
+ // Process children in reverse to handle removals safely
368
+ const children = Array.from(parent.children);
369
+ for (const child of children) {
370
+ // Skip bq-for elements — they're handled by parent
371
+ if (child.hasAttribute(`${prefix}-for`)) {
372
+ // Process the for directive on this element
373
+ const keep = processSSRElement(child, context, prefix, doc);
374
+ if (!keep) {
375
+ child.remove();
376
+ }
377
+ continue;
378
+ }
379
+
380
+ const keep = processSSRElement(child, context, prefix, doc);
381
+ if (!keep) {
382
+ child.remove();
383
+ continue;
384
+ }
385
+
386
+ // Recurse into children
387
+ processSSRChildren(child, context, prefix, doc);
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Strips all directive attributes (bq-*) from an element and its descendants.
393
+ * @internal
394
+ */
395
+ const stripDirectiveAttributes = (el: Element, prefix: string): void => {
396
+ // Remove directive attributes from this element
397
+ const attrs = Array.from(el.attributes);
398
+ for (const attr of attrs) {
399
+ if (attr.name.startsWith(`${prefix}-`) || attr.name.startsWith(':') || attr.name === ':key') {
400
+ el.removeAttribute(attr.name);
401
+ }
402
+ }
403
+
404
+ // Recurse into children
405
+ for (const child of Array.from(el.children)) {
406
+ stripDirectiveAttributes(child, prefix);
407
+ }
408
+ };
409
+
410
+ /**
411
+ * Server-side renders a bQuery template to an HTML string.
412
+ *
413
+ * Takes an HTML template with bQuery directives (bq-text, bq-if, bq-for, etc.)
414
+ * and a data context, then evaluates the directives to produce a static HTML string.
415
+ * This HTML can be sent to the client and later hydrated with `mount()` using
416
+ * `{ hydrate: true }`.
417
+ *
418
+ * Supported directives:
419
+ * - `bq-text` — Sets text content
420
+ * - `bq-html` — Sets innerHTML
421
+ * - `bq-if` — Conditional rendering (removes element if falsy)
422
+ * - `bq-show` — Toggle visibility via `display: none`
423
+ * - `bq-class` — Dynamic class binding (object or expression syntax)
424
+ * - `bq-style` — Dynamic inline styles
425
+ * - `bq-for` — List rendering
426
+ * - `bq-bind:attr` — Dynamic attribute binding
427
+ *
428
+ * @param template - HTML template string with bq-* directives
429
+ * @param data - Plain data object (signals will be unwrapped automatically)
430
+ * @param options - Rendering options
431
+ * @returns SSR result with HTML string and optional store state
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * import { renderToString } from '@bquery/bquery/ssr';
436
+ * import { signal } from '@bquery/bquery/reactive';
437
+ *
438
+ * const result = renderToString(
439
+ * '<div><h1 bq-text="title"></h1><p bq-if="showBody">Hello!</p></div>',
440
+ * { title: 'Welcome', showBody: true }
441
+ * );
442
+ *
443
+ * console.log(result.html);
444
+ * // '<div><h1>Welcome</h1><p>Hello!</p></div>'
445
+ * ```
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * // With bq-for list rendering
450
+ * const result = renderToString(
451
+ * '<ul><li bq-for="item in items" bq-text="item.name"></li></ul>',
452
+ * { items: [{ name: 'Alice' }, { name: 'Bob' }] }
453
+ * );
454
+ *
455
+ * console.log(result.html);
456
+ * // '<ul><li>Alice</li><li>Bob</li></ul>'
457
+ * ```
458
+ */
459
+ export const renderToString = (
460
+ template: string,
461
+ data: BindingContext,
462
+ options: RenderOptions = {}
463
+ ): SSRResult => {
464
+ const { prefix = 'bq', stripDirectives = false, includeStoreState = false } = options;
465
+
466
+ if (!template || typeof template !== 'string') {
467
+ throw new Error('bQuery SSR: template must be a non-empty string.');
468
+ }
469
+
470
+ if (typeof DOMParser === 'undefined') {
471
+ throw new Error(
472
+ 'bQuery SSR: DOMParser is not available in this environment. Provide a DOMParser-compatible implementation before calling renderToString().'
473
+ );
474
+ }
475
+
476
+ // Create a DOM document for processing
477
+ const parser = new DOMParser();
478
+ const doc = parser.parseFromString(template.trim(), 'text/html');
479
+ const body = doc.body || doc.documentElement;
480
+
481
+ if (!body) {
482
+ throw new Error('bQuery SSR: Failed to parse template.');
483
+ }
484
+
485
+ // Process all children of the body
486
+ processSSRChildren(body, data, prefix, doc);
487
+
488
+ // Strip directive attributes if requested
489
+ if (stripDirectives) {
490
+ for (const child of Array.from(body.children)) {
491
+ stripDirectiveAttributes(child, prefix);
492
+ }
493
+ }
494
+
495
+ let html = '';
496
+ for (const child of body.childNodes) {
497
+ html += serializeSSRNode(child);
498
+ }
499
+
500
+ // Handle store state serialization
501
+ let storeState: string | undefined;
502
+ if (includeStoreState) {
503
+ const storeIds = Array.isArray(includeStoreState) ? includeStoreState : undefined;
504
+ storeState = serializeStoreState({ storeIds }).stateJson;
505
+ }
506
+
507
+ return { html, storeState };
508
+ };