@bquery/bquery 1.7.0 → 1.8.1

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 (262) hide show
  1. package/README.md +760 -716
  2. package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
  3. package/dist/a11y-DVBCy09c.js.map +1 -0
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/component/library.d.ts.map +1 -1
  6. package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
  7. package/dist/component-L3-JfOFz.js.map +1 -0
  8. package/dist/component.es.mjs +1 -1
  9. package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
  10. package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
  11. package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
  12. package/dist/constraints-D5RHQLmP.js.map +1 -0
  13. package/dist/core/collection.d.ts +86 -0
  14. package/dist/core/collection.d.ts.map +1 -1
  15. package/dist/core/element.d.ts +28 -0
  16. package/dist/core/element.d.ts.map +1 -1
  17. package/dist/core/shared.d.ts +6 -0
  18. package/dist/core/shared.d.ts.map +1 -1
  19. package/dist/core-DdtZHzsS.js +168 -0
  20. package/dist/core-DdtZHzsS.js.map +1 -0
  21. package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
  22. package/dist/core-EMYSLzaT.js.map +1 -0
  23. package/dist/core.es.mjs +48 -47
  24. package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
  25. package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
  26. package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
  27. package/dist/devtools-BhB2iDPT.js.map +1 -0
  28. package/dist/devtools.es.mjs +1 -1
  29. package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
  30. package/dist/dnd-NwZBYh4l.js.map +1 -0
  31. package/dist/dnd.es.mjs +1 -1
  32. package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
  33. package/dist/env-CTdvLaH2.js.map +1 -0
  34. package/dist/forms/create-form.d.ts.map +1 -1
  35. package/dist/forms/index.d.ts +3 -2
  36. package/dist/forms/index.d.ts.map +1 -1
  37. package/dist/forms/types.d.ts +46 -0
  38. package/dist/forms/types.d.ts.map +1 -1
  39. package/dist/forms/use-field.d.ts +34 -0
  40. package/dist/forms/use-field.d.ts.map +1 -0
  41. package/dist/forms/validators.d.ts +25 -0
  42. package/dist/forms/validators.d.ts.map +1 -1
  43. package/dist/forms-UcRHsYxC.js +227 -0
  44. package/dist/forms-UcRHsYxC.js.map +1 -0
  45. package/dist/forms.es.mjs +14 -12
  46. package/dist/full.d.ts +17 -26
  47. package/dist/full.d.ts.map +1 -1
  48. package/dist/full.es.mjs +206 -181
  49. package/dist/full.iife.js +33 -33
  50. package/dist/full.iife.js.map +1 -1
  51. package/dist/full.umd.js +33 -33
  52. package/dist/full.umd.js.map +1 -1
  53. package/dist/function-Cybd57JV.js +33 -0
  54. package/dist/function-Cybd57JV.js.map +1 -0
  55. package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
  56. package/dist/i18n-kuF6Ekj6.js.map +1 -0
  57. package/dist/i18n.es.mjs +1 -1
  58. package/dist/index.es.mjs +251 -228
  59. package/dist/media/breakpoints.d.ts.map +1 -1
  60. package/dist/media/types.d.ts +2 -2
  61. package/dist/media/types.d.ts.map +1 -1
  62. package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
  63. package/dist/media-i-fB5WxI.js.map +1 -0
  64. package/dist/media.es.mjs +1 -1
  65. package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
  66. package/dist/motion-BJsAuULb.js.map +1 -0
  67. package/dist/motion.es.mjs +1 -1
  68. package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
  69. package/dist/mount-B4Y8bk8Z.js.map +1 -0
  70. package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
  71. package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
  72. package/dist/platform.es.mjs +2 -2
  73. package/dist/plugin/registry.d.ts.map +1 -1
  74. package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
  75. package/dist/plugin-C2WuC8SF.js.map +1 -0
  76. package/dist/plugin.es.mjs +1 -1
  77. package/dist/reactive/async-data.d.ts +28 -3
  78. package/dist/reactive/async-data.d.ts.map +1 -1
  79. package/dist/reactive/computed.d.ts +3 -0
  80. package/dist/reactive/computed.d.ts.map +1 -1
  81. package/dist/reactive/effect.d.ts +3 -0
  82. package/dist/reactive/effect.d.ts.map +1 -1
  83. package/dist/reactive/http.d.ts +194 -0
  84. package/dist/reactive/http.d.ts.map +1 -0
  85. package/dist/reactive/index.d.ts +2 -2
  86. package/dist/reactive/index.d.ts.map +1 -1
  87. package/dist/reactive/pagination.d.ts +126 -0
  88. package/dist/reactive/pagination.d.ts.map +1 -0
  89. package/dist/reactive/polling.d.ts +55 -0
  90. package/dist/reactive/polling.d.ts.map +1 -0
  91. package/dist/reactive/readonly.d.ts +20 -1
  92. package/dist/reactive/readonly.d.ts.map +1 -1
  93. package/dist/reactive/rest.d.ts +293 -0
  94. package/dist/reactive/rest.d.ts.map +1 -0
  95. package/dist/reactive/scope.d.ts +140 -0
  96. package/dist/reactive/scope.d.ts.map +1 -0
  97. package/dist/reactive/signal.d.ts +16 -2
  98. package/dist/reactive/signal.d.ts.map +1 -1
  99. package/dist/reactive/to-value.d.ts +57 -0
  100. package/dist/reactive/to-value.d.ts.map +1 -0
  101. package/dist/reactive/websocket.d.ts +285 -0
  102. package/dist/reactive/websocket.d.ts.map +1 -0
  103. package/dist/reactive-DwkhUJfP.js +1148 -0
  104. package/dist/reactive-DwkhUJfP.js.map +1 -0
  105. package/dist/reactive.es.mjs +38 -19
  106. package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
  107. package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
  108. package/dist/router/constraints.d.ts.map +1 -1
  109. package/dist/router/index.d.ts +1 -1
  110. package/dist/router/index.d.ts.map +1 -1
  111. package/dist/router/router.d.ts.map +1 -1
  112. package/dist/router/state.d.ts +25 -2
  113. package/dist/router/state.d.ts.map +1 -1
  114. package/dist/router-CQikC9Ed.js +492 -0
  115. package/dist/router-CQikC9Ed.js.map +1 -0
  116. package/dist/router.es.mjs +9 -8
  117. package/dist/ssr/hydrate.d.ts.map +1 -1
  118. package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
  119. package/dist/ssr-_dAcGdzu.js.map +1 -0
  120. package/dist/ssr.es.mjs +1 -1
  121. package/dist/store/persisted.d.ts.map +1 -1
  122. package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
  123. package/dist/store-Cb3gPRve.js.map +1 -0
  124. package/dist/store.es.mjs +2 -2
  125. package/dist/storybook.es.mjs.map +1 -1
  126. package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
  127. package/dist/testing-C5Sjfsna.js.map +1 -0
  128. package/dist/testing.es.mjs +1 -1
  129. package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
  130. package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
  131. package/dist/untrack-D0fnO5k2.js +36 -0
  132. package/dist/untrack-D0fnO5k2.js.map +1 -0
  133. package/dist/view/custom-directives.d.ts.map +1 -1
  134. package/dist/view.es.mjs +4 -4
  135. package/package.json +177 -177
  136. package/src/a11y/announce.ts +131 -131
  137. package/src/a11y/audit.ts +314 -314
  138. package/src/a11y/index.ts +68 -68
  139. package/src/a11y/media-preferences.ts +255 -255
  140. package/src/a11y/roving-tab-index.ts +164 -164
  141. package/src/a11y/skip-link.ts +255 -255
  142. package/src/a11y/trap-focus.ts +184 -184
  143. package/src/a11y/types.ts +183 -183
  144. package/src/component/component.ts +599 -599
  145. package/src/component/html.ts +153 -153
  146. package/src/component/index.ts +52 -52
  147. package/src/component/library.ts +540 -542
  148. package/src/component/scope.ts +212 -212
  149. package/src/component/types.ts +310 -310
  150. package/src/core/collection.ts +876 -707
  151. package/src/core/element.ts +1015 -981
  152. package/src/core/env.ts +60 -60
  153. package/src/core/index.ts +49 -49
  154. package/src/core/shared.ts +77 -62
  155. package/src/core/utils/index.ts +148 -148
  156. package/src/devtools/devtools.ts +410 -410
  157. package/src/devtools/index.ts +48 -48
  158. package/src/devtools/types.ts +104 -104
  159. package/src/dnd/draggable.ts +296 -296
  160. package/src/dnd/droppable.ts +228 -228
  161. package/src/dnd/index.ts +62 -62
  162. package/src/dnd/sortable.ts +307 -307
  163. package/src/dnd/types.ts +293 -293
  164. package/src/forms/create-form.ts +320 -278
  165. package/src/forms/index.ts +70 -65
  166. package/src/forms/types.ts +203 -154
  167. package/src/forms/use-field.ts +231 -0
  168. package/src/forms/validators.ts +294 -265
  169. package/src/full.ts +554 -480
  170. package/src/i18n/formatting.ts +67 -67
  171. package/src/i18n/i18n.ts +200 -200
  172. package/src/i18n/index.ts +67 -67
  173. package/src/i18n/translate.ts +182 -182
  174. package/src/i18n/types.ts +171 -171
  175. package/src/index.ts +108 -108
  176. package/src/media/battery.ts +116 -116
  177. package/src/media/breakpoints.ts +129 -131
  178. package/src/media/clipboard.ts +80 -80
  179. package/src/media/device-sensors.ts +158 -158
  180. package/src/media/geolocation.ts +119 -119
  181. package/src/media/index.ts +76 -76
  182. package/src/media/media-query.ts +92 -92
  183. package/src/media/network.ts +115 -115
  184. package/src/media/types.ts +177 -177
  185. package/src/media/viewport.ts +84 -84
  186. package/src/motion/index.ts +57 -57
  187. package/src/motion/morph.ts +151 -151
  188. package/src/motion/parallax.ts +120 -120
  189. package/src/motion/reduced-motion.ts +66 -66
  190. package/src/motion/types.ts +271 -271
  191. package/src/motion/typewriter.ts +164 -164
  192. package/src/plugin/index.ts +37 -37
  193. package/src/plugin/registry.ts +284 -269
  194. package/src/plugin/types.ts +137 -137
  195. package/src/reactive/async-data.ts +250 -29
  196. package/src/reactive/computed.ts +144 -130
  197. package/src/reactive/effect.ts +29 -6
  198. package/src/reactive/http.ts +790 -0
  199. package/src/reactive/index.ts +60 -0
  200. package/src/reactive/pagination.ts +317 -0
  201. package/src/reactive/polling.ts +179 -0
  202. package/src/reactive/readonly.ts +52 -8
  203. package/src/reactive/rest.ts +859 -0
  204. package/src/reactive/scope.ts +276 -0
  205. package/src/reactive/signal.ts +61 -1
  206. package/src/reactive/to-value.ts +71 -0
  207. package/src/reactive/websocket.ts +849 -0
  208. package/src/router/bq-link.ts +279 -279
  209. package/src/router/constraints.ts +204 -201
  210. package/src/router/index.ts +49 -49
  211. package/src/router/match.ts +312 -312
  212. package/src/router/path-pattern.ts +52 -52
  213. package/src/router/query.ts +38 -38
  214. package/src/router/router.ts +421 -402
  215. package/src/router/state.ts +51 -3
  216. package/src/router/types.ts +139 -139
  217. package/src/router/use-route.ts +68 -68
  218. package/src/router/utils.ts +157 -157
  219. package/src/security/index.ts +12 -12
  220. package/src/ssr/hydrate.ts +84 -82
  221. package/src/ssr/index.ts +70 -70
  222. package/src/ssr/render.ts +508 -508
  223. package/src/ssr/serialize.ts +296 -296
  224. package/src/ssr/types.ts +81 -81
  225. package/src/store/create-store.ts +467 -467
  226. package/src/store/index.ts +27 -27
  227. package/src/store/persisted.ts +245 -249
  228. package/src/store/types.ts +247 -247
  229. package/src/store/utils.ts +135 -135
  230. package/src/storybook/index.ts +480 -480
  231. package/src/testing/index.ts +42 -42
  232. package/src/testing/testing.ts +593 -593
  233. package/src/testing/types.ts +170 -170
  234. package/src/view/custom-directives.ts +28 -30
  235. package/src/view/evaluate.ts +292 -292
  236. package/src/view/process.ts +108 -108
  237. package/dist/a11y-C5QOVvRn.js.map +0 -1
  238. package/dist/component-CuuTijA6.js.map +0 -1
  239. package/dist/constraints-3lV9yyBw.js.map +0 -1
  240. package/dist/core-Cjl7GUu8.js.map +0 -1
  241. package/dist/core-DnlyjbF2.js +0 -112
  242. package/dist/core-DnlyjbF2.js.map +0 -1
  243. package/dist/custom-directives-7wAShnnd.js.map +0 -1
  244. package/dist/devtools-D2fQLhDN.js.map +0 -1
  245. package/dist/dnd-B8EgyzaI.js.map +0 -1
  246. package/dist/env-NeVmr4Gf.js.map +0 -1
  247. package/dist/forms-C3yovgH9.js +0 -141
  248. package/dist/forms-C3yovgH9.js.map +0 -1
  249. package/dist/i18n-BnnhTFOS.js.map +0 -1
  250. package/dist/media-Di2Ta22s.js.map +0 -1
  251. package/dist/motion-qPj_TYGv.js.map +0 -1
  252. package/dist/mount-SM07RUa6.js.map +0 -1
  253. package/dist/plugin-cPoOHFLY.js.map +0 -1
  254. package/dist/reactive-Cfv0RK6x.js +0 -233
  255. package/dist/reactive-Cfv0RK6x.js.map +0 -1
  256. package/dist/router-BrthaP_z.js +0 -473
  257. package/dist/router-BrthaP_z.js.map +0 -1
  258. package/dist/ssr-B2qd_WBB.js.map +0 -1
  259. package/dist/store-DWpyH6p5.js.map +0 -1
  260. package/dist/testing-CsqjNUyy.js.map +0 -1
  261. package/dist/untrack-DJVQQ2WM.js +0 -33
  262. package/dist/untrack-DJVQQ2WM.js.map +0 -1
@@ -1,292 +1,292 @@
1
- import { isPrototypePollutionKey } from '../core/utils/object';
2
- import { isComputed, isSignal, type Signal } from '../reactive/index';
3
- import type { BindingContext } from './types';
4
-
5
- /** Maximum number of cached expression functions before LRU eviction */
6
- const MAX_CACHE_SIZE = 500;
7
-
8
- /** Compiled function type for expression evaluation */
9
- type CompiledFn = (ctx: BindingContext) => unknown;
10
-
11
- /**
12
- * Simple LRU cache for compiled expression functions.
13
- * Uses Map's insertion order to track recency - accessed items are re-inserted.
14
- * @internal
15
- */
16
- class LRUCache {
17
- private cache = new Map<string, CompiledFn>();
18
- private maxSize: number;
19
-
20
- constructor(maxSize: number) {
21
- this.maxSize = maxSize;
22
- }
23
-
24
- get(key: string): CompiledFn | undefined {
25
- const value = this.cache.get(key);
26
- if (value !== undefined) {
27
- // Move to end (most recently used) by re-inserting
28
- this.cache.delete(key);
29
- this.cache.set(key, value);
30
- }
31
- return value;
32
- }
33
-
34
- set(key: string, value: CompiledFn): void {
35
- // Delete first if exists to update insertion order
36
- if (this.cache.has(key)) {
37
- this.cache.delete(key);
38
- } else if (this.cache.size >= this.maxSize) {
39
- // Evict oldest (first) entry
40
- const oldest = this.cache.keys().next().value;
41
- if (oldest !== undefined) {
42
- this.cache.delete(oldest);
43
- }
44
- }
45
- this.cache.set(key, value);
46
- }
47
-
48
- clear(): void {
49
- this.cache.clear();
50
- }
51
-
52
- get size(): number {
53
- return this.cache.size;
54
- }
55
- }
56
-
57
- /** LRU cache for compiled evaluate functions, keyed by expression string */
58
- const evaluateCache = new LRUCache(MAX_CACHE_SIZE);
59
-
60
- /** LRU cache for compiled evaluateRaw functions, keyed by expression string */
61
- const evaluateRawCache = new LRUCache(MAX_CACHE_SIZE);
62
-
63
- /**
64
- * Clears all cached compiled expression functions.
65
- * Call this when unmounting views or to free memory after heavy template usage.
66
- *
67
- * @example
68
- * ```ts
69
- * import { clearExpressionCache } from 'bquery/view';
70
- *
71
- * // After destroying a view or when cleaning up
72
- * clearExpressionCache();
73
- * ```
74
- */
75
- export const clearExpressionCache = (): void => {
76
- evaluateCache.clear();
77
- evaluateRawCache.clear();
78
- };
79
-
80
- /**
81
- * Creates a proxy that lazily unwraps signals/computed only when accessed.
82
- * This avoids subscribing to signals that aren't referenced in the expression.
83
- * @internal
84
- */
85
- const createLazyContext = (context: BindingContext): BindingContext =>
86
- new Proxy(context, {
87
- get(target, prop: string | symbol) {
88
- // Only handle string keys for BindingContext indexing
89
- if (typeof prop !== 'string') {
90
- return Reflect.get(target, prop);
91
- }
92
- const value = target[prop];
93
- // Auto-unwrap signals/computed only when actually accessed
94
- if (isSignal(value) || isComputed(value)) {
95
- return (value as Signal<unknown>).value;
96
- }
97
- return value;
98
- },
99
- has(target, prop: string | symbol) {
100
- // Required for `with` statement to resolve identifiers correctly
101
- if (typeof prop !== 'string') {
102
- return Reflect.has(target, prop);
103
- }
104
- return prop in target;
105
- },
106
- });
107
-
108
- /**
109
- * Evaluates an expression in the given context using `new Function()`.
110
- *
111
- * Signals and computed values in the context are lazily unwrapped only when
112
- * accessed by the expression, avoiding unnecessary subscriptions to unused values.
113
- *
114
- * @security **WARNING:** This function uses dynamic code execution via `new Function()`.
115
- * - NEVER pass expressions derived from user input or untrusted sources
116
- * - Expressions should only come from developer-controlled templates
117
- * - Malicious expressions can access and exfiltrate context data
118
- * - Consider this equivalent to `eval()` in terms of security implications
119
- *
120
- * @internal
121
- */
122
- export const evaluate = <T = unknown>(expression: string, context: BindingContext): T => {
123
- try {
124
- // Create a proxy that lazily unwraps signals/computed on access
125
- const lazyContext = createLazyContext(context);
126
-
127
- // Use cached function or compile and cache a new one
128
- let fn = evaluateCache.get(expression);
129
- if (!fn) {
130
- // Use `with` to enable direct property access from proxy scope.
131
- // Note: `new Function()` runs in non-strict mode, so `with` is allowed.
132
- fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
133
- ctx: BindingContext
134
- ) => unknown;
135
- evaluateCache.set(expression, fn);
136
- }
137
- return fn(lazyContext) as T;
138
- } catch (error) {
139
- console.error(`bQuery view: Error evaluating "${expression}"`, error);
140
- return undefined as T;
141
- }
142
- };
143
-
144
- /**
145
- * Evaluates an expression and returns the raw value (for signal access).
146
- *
147
- * @security **WARNING:** Uses dynamic code execution. See {@link evaluate} for security notes.
148
- * @internal
149
- */
150
- export const evaluateRaw = <T = unknown>(expression: string, context: BindingContext): T => {
151
- try {
152
- // Use cached function or compile and cache a new one
153
- let fn = evaluateRawCache.get(expression);
154
- if (!fn) {
155
- // Use `with` to enable direct property access from context scope.
156
- // Unlike `evaluate`, we don't use a lazy proxy - values are accessed directly.
157
- fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
158
- ctx: BindingContext
159
- ) => unknown;
160
- evaluateRawCache.set(expression, fn);
161
- }
162
- return fn(context) as T;
163
- } catch (error) {
164
- console.error(`bQuery view: Error evaluating "${expression}"`, error);
165
- return undefined as T;
166
- }
167
- };
168
-
169
- /**
170
- * Parses object expression like "{ active: isActive, disabled: !enabled }".
171
- * Handles nested structures like function calls, arrays, and template literals.
172
- * @internal
173
- */
174
- export const parseObjectExpression = (expression: string): Record<string, string> => {
175
- const result: Record<string, string> = {};
176
-
177
- // Remove outer braces and trim
178
- const inner = expression
179
- .trim()
180
- .replace(/^\{|\}$/g, '')
181
- .trim();
182
- if (!inner) return result;
183
-
184
- // Split by comma at depth 0, respecting strings and nesting
185
- const parts: string[] = [];
186
- let current = '';
187
- let depth = 0;
188
- let inString: string | null = null;
189
-
190
- for (let i = 0; i < inner.length; i++) {
191
- const char = inner[i];
192
-
193
- // Handle string literals: count consecutive backslashes before a quote
194
- // to correctly distinguish escaped quotes from end-of-string
195
- if (char === '"' || char === "'" || char === '`') {
196
- let backslashCount = 0;
197
- let j = i - 1;
198
- while (j >= 0 && inner[j] === '\\') {
199
- backslashCount++;
200
- j--;
201
- }
202
- // Quote is escaped only if preceded by an odd number of backslashes
203
- if (backslashCount % 2 === 0) {
204
- if (inString === null) {
205
- inString = char;
206
- } else if (inString === char) {
207
- inString = null;
208
- }
209
- }
210
- current += char;
211
- continue;
212
- }
213
-
214
- // Skip if inside string
215
- if (inString !== null) {
216
- current += char;
217
- continue;
218
- }
219
-
220
- // Track nesting depth for parentheses, brackets, and braces
221
- if (char === '(' || char === '[' || char === '{') {
222
- depth++;
223
- current += char;
224
- } else if (char === ')' || char === ']' || char === '}') {
225
- depth--;
226
- current += char;
227
- } else if (char === ',' && depth === 0) {
228
- // Top-level comma - split point
229
- parts.push(current.trim());
230
- current = '';
231
- } else {
232
- current += char;
233
- }
234
- }
235
-
236
- // Add the last part
237
- if (current.trim()) {
238
- parts.push(current.trim());
239
- }
240
-
241
- // Parse each part to extract key and value
242
- for (const part of parts) {
243
- // Find the first colon at depth 0 (to handle ternary operators in values)
244
- let colonIndex = -1;
245
- let partDepth = 0;
246
- let partInString: string | null = null;
247
-
248
- for (let i = 0; i < part.length; i++) {
249
- const char = part[i];
250
-
251
- if (char === '"' || char === "'" || char === '`') {
252
- let backslashCount = 0;
253
- let j = i - 1;
254
- while (j >= 0 && part[j] === '\\') {
255
- backslashCount++;
256
- j--;
257
- }
258
- if (backslashCount % 2 === 0) {
259
- if (partInString === null) {
260
- partInString = char;
261
- } else if (partInString === char) {
262
- partInString = null;
263
- }
264
- }
265
- continue;
266
- }
267
-
268
- if (partInString !== null) continue;
269
-
270
- if (char === '(' || char === '[' || char === '{') {
271
- partDepth++;
272
- } else if (char === ')' || char === ']' || char === '}') {
273
- partDepth--;
274
- } else if (char === ':' && partDepth === 0) {
275
- colonIndex = i;
276
- break;
277
- }
278
- }
279
-
280
- if (colonIndex > -1) {
281
- const key = part
282
- .slice(0, colonIndex)
283
- .trim()
284
- .replace(/^['"]|['"]$/g, '');
285
- if (isPrototypePollutionKey(key)) continue;
286
- const value = part.slice(colonIndex + 1).trim();
287
- result[key] = value;
288
- }
289
- }
290
-
291
- return result;
292
- };
1
+ import { isPrototypePollutionKey } from '../core/utils/object';
2
+ import { isComputed, isSignal, type Signal } from '../reactive/index';
3
+ import type { BindingContext } from './types';
4
+
5
+ /** Maximum number of cached expression functions before LRU eviction */
6
+ const MAX_CACHE_SIZE = 500;
7
+
8
+ /** Compiled function type for expression evaluation */
9
+ type CompiledFn = (ctx: BindingContext) => unknown;
10
+
11
+ /**
12
+ * Simple LRU cache for compiled expression functions.
13
+ * Uses Map's insertion order to track recency - accessed items are re-inserted.
14
+ * @internal
15
+ */
16
+ class LRUCache {
17
+ private cache = new Map<string, CompiledFn>();
18
+ private maxSize: number;
19
+
20
+ constructor(maxSize: number) {
21
+ this.maxSize = maxSize;
22
+ }
23
+
24
+ get(key: string): CompiledFn | undefined {
25
+ const value = this.cache.get(key);
26
+ if (value !== undefined) {
27
+ // Move to end (most recently used) by re-inserting
28
+ this.cache.delete(key);
29
+ this.cache.set(key, value);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ set(key: string, value: CompiledFn): void {
35
+ // Delete first if exists to update insertion order
36
+ if (this.cache.has(key)) {
37
+ this.cache.delete(key);
38
+ } else if (this.cache.size >= this.maxSize) {
39
+ // Evict oldest (first) entry
40
+ const oldest = this.cache.keys().next().value;
41
+ if (oldest !== undefined) {
42
+ this.cache.delete(oldest);
43
+ }
44
+ }
45
+ this.cache.set(key, value);
46
+ }
47
+
48
+ clear(): void {
49
+ this.cache.clear();
50
+ }
51
+
52
+ get size(): number {
53
+ return this.cache.size;
54
+ }
55
+ }
56
+
57
+ /** LRU cache for compiled evaluate functions, keyed by expression string */
58
+ const evaluateCache = new LRUCache(MAX_CACHE_SIZE);
59
+
60
+ /** LRU cache for compiled evaluateRaw functions, keyed by expression string */
61
+ const evaluateRawCache = new LRUCache(MAX_CACHE_SIZE);
62
+
63
+ /**
64
+ * Clears all cached compiled expression functions.
65
+ * Call this when unmounting views or to free memory after heavy template usage.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { clearExpressionCache } from 'bquery/view';
70
+ *
71
+ * // After destroying a view or when cleaning up
72
+ * clearExpressionCache();
73
+ * ```
74
+ */
75
+ export const clearExpressionCache = (): void => {
76
+ evaluateCache.clear();
77
+ evaluateRawCache.clear();
78
+ };
79
+
80
+ /**
81
+ * Creates a proxy that lazily unwraps signals/computed only when accessed.
82
+ * This avoids subscribing to signals that aren't referenced in the expression.
83
+ * @internal
84
+ */
85
+ const createLazyContext = (context: BindingContext): BindingContext =>
86
+ new Proxy(context, {
87
+ get(target, prop: string | symbol) {
88
+ // Only handle string keys for BindingContext indexing
89
+ if (typeof prop !== 'string') {
90
+ return Reflect.get(target, prop);
91
+ }
92
+ const value = target[prop];
93
+ // Auto-unwrap signals/computed only when actually accessed
94
+ if (isSignal(value) || isComputed(value)) {
95
+ return (value as Signal<unknown>).value;
96
+ }
97
+ return value;
98
+ },
99
+ has(target, prop: string | symbol) {
100
+ // Required for `with` statement to resolve identifiers correctly
101
+ if (typeof prop !== 'string') {
102
+ return Reflect.has(target, prop);
103
+ }
104
+ return prop in target;
105
+ },
106
+ });
107
+
108
+ /**
109
+ * Evaluates an expression in the given context using `new Function()`.
110
+ *
111
+ * Signals and computed values in the context are lazily unwrapped only when
112
+ * accessed by the expression, avoiding unnecessary subscriptions to unused values.
113
+ *
114
+ * @security **WARNING:** This function uses dynamic code execution via `new Function()`.
115
+ * - NEVER pass expressions derived from user input or untrusted sources
116
+ * - Expressions should only come from developer-controlled templates
117
+ * - Malicious expressions can access and exfiltrate context data
118
+ * - Consider this equivalent to `eval()` in terms of security implications
119
+ *
120
+ * @internal
121
+ */
122
+ export const evaluate = <T = unknown>(expression: string, context: BindingContext): T => {
123
+ try {
124
+ // Create a proxy that lazily unwraps signals/computed on access
125
+ const lazyContext = createLazyContext(context);
126
+
127
+ // Use cached function or compile and cache a new one
128
+ let fn = evaluateCache.get(expression);
129
+ if (!fn) {
130
+ // Use `with` to enable direct property access from proxy scope.
131
+ // Note: `new Function()` runs in non-strict mode, so `with` is allowed.
132
+ fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
133
+ ctx: BindingContext
134
+ ) => unknown;
135
+ evaluateCache.set(expression, fn);
136
+ }
137
+ return fn(lazyContext) as T;
138
+ } catch (error) {
139
+ console.error(`bQuery view: Error evaluating "${expression}"`, error);
140
+ return undefined as T;
141
+ }
142
+ };
143
+
144
+ /**
145
+ * Evaluates an expression and returns the raw value (for signal access).
146
+ *
147
+ * @security **WARNING:** Uses dynamic code execution. See {@link evaluate} for security notes.
148
+ * @internal
149
+ */
150
+ export const evaluateRaw = <T = unknown>(expression: string, context: BindingContext): T => {
151
+ try {
152
+ // Use cached function or compile and cache a new one
153
+ let fn = evaluateRawCache.get(expression);
154
+ if (!fn) {
155
+ // Use `with` to enable direct property access from context scope.
156
+ // Unlike `evaluate`, we don't use a lazy proxy - values are accessed directly.
157
+ fn = new Function('$ctx', `with($ctx) { return (${expression}); }`) as (
158
+ ctx: BindingContext
159
+ ) => unknown;
160
+ evaluateRawCache.set(expression, fn);
161
+ }
162
+ return fn(context) as T;
163
+ } catch (error) {
164
+ console.error(`bQuery view: Error evaluating "${expression}"`, error);
165
+ return undefined as T;
166
+ }
167
+ };
168
+
169
+ /**
170
+ * Parses object expression like "{ active: isActive, disabled: !enabled }".
171
+ * Handles nested structures like function calls, arrays, and template literals.
172
+ * @internal
173
+ */
174
+ export const parseObjectExpression = (expression: string): Record<string, string> => {
175
+ const result: Record<string, string> = {};
176
+
177
+ // Remove outer braces and trim
178
+ const inner = expression
179
+ .trim()
180
+ .replace(/^\{|\}$/g, '')
181
+ .trim();
182
+ if (!inner) return result;
183
+
184
+ // Split by comma at depth 0, respecting strings and nesting
185
+ const parts: string[] = [];
186
+ let current = '';
187
+ let depth = 0;
188
+ let inString: string | null = null;
189
+
190
+ for (let i = 0; i < inner.length; i++) {
191
+ const char = inner[i];
192
+
193
+ // Handle string literals: count consecutive backslashes before a quote
194
+ // to correctly distinguish escaped quotes from end-of-string
195
+ if (char === '"' || char === "'" || char === '`') {
196
+ let backslashCount = 0;
197
+ let j = i - 1;
198
+ while (j >= 0 && inner[j] === '\\') {
199
+ backslashCount++;
200
+ j--;
201
+ }
202
+ // Quote is escaped only if preceded by an odd number of backslashes
203
+ if (backslashCount % 2 === 0) {
204
+ if (inString === null) {
205
+ inString = char;
206
+ } else if (inString === char) {
207
+ inString = null;
208
+ }
209
+ }
210
+ current += char;
211
+ continue;
212
+ }
213
+
214
+ // Skip if inside string
215
+ if (inString !== null) {
216
+ current += char;
217
+ continue;
218
+ }
219
+
220
+ // Track nesting depth for parentheses, brackets, and braces
221
+ if (char === '(' || char === '[' || char === '{') {
222
+ depth++;
223
+ current += char;
224
+ } else if (char === ')' || char === ']' || char === '}') {
225
+ depth--;
226
+ current += char;
227
+ } else if (char === ',' && depth === 0) {
228
+ // Top-level comma - split point
229
+ parts.push(current.trim());
230
+ current = '';
231
+ } else {
232
+ current += char;
233
+ }
234
+ }
235
+
236
+ // Add the last part
237
+ if (current.trim()) {
238
+ parts.push(current.trim());
239
+ }
240
+
241
+ // Parse each part to extract key and value
242
+ for (const part of parts) {
243
+ // Find the first colon at depth 0 (to handle ternary operators in values)
244
+ let colonIndex = -1;
245
+ let partDepth = 0;
246
+ let partInString: string | null = null;
247
+
248
+ for (let i = 0; i < part.length; i++) {
249
+ const char = part[i];
250
+
251
+ if (char === '"' || char === "'" || char === '`') {
252
+ let backslashCount = 0;
253
+ let j = i - 1;
254
+ while (j >= 0 && part[j] === '\\') {
255
+ backslashCount++;
256
+ j--;
257
+ }
258
+ if (backslashCount % 2 === 0) {
259
+ if (partInString === null) {
260
+ partInString = char;
261
+ } else if (partInString === char) {
262
+ partInString = null;
263
+ }
264
+ }
265
+ continue;
266
+ }
267
+
268
+ if (partInString !== null) continue;
269
+
270
+ if (char === '(' || char === '[' || char === '{') {
271
+ partDepth++;
272
+ } else if (char === ')' || char === ']' || char === '}') {
273
+ partDepth--;
274
+ } else if (char === ':' && partDepth === 0) {
275
+ colonIndex = i;
276
+ break;
277
+ }
278
+ }
279
+
280
+ if (colonIndex > -1) {
281
+ const key = part
282
+ .slice(0, colonIndex)
283
+ .trim()
284
+ .replace(/^['"]|['"]$/g, '');
285
+ if (isPrototypePollutionKey(key)) continue;
286
+ const value = part.slice(colonIndex + 1).trim();
287
+ result[key] = value;
288
+ }
289
+ }
290
+
291
+ return result;
292
+ };