zero-query 0.9.1 → 0.9.6
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.
- package/README.md +11 -8
- package/cli/commands/dev/devtools/js/elements.js +5 -0
- package/cli/scaffold/app/app.js +1 -1
- package/cli/scaffold/global.css +1 -2
- package/cli/scaffold/index.html +2 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +429 -35
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +46 -2
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +98 -8
- package/src/core.js +3 -1
- package/src/expression.js +103 -16
- package/src/http.js +6 -2
- package/src/router.js +6 -1
- package/src/utils.js +191 -5
- package/tests/audit.test.js +4030 -0
- package/tests/component.test.js +1185 -0
- package/tests/core.test.js +41 -0
- package/tests/expression.test.js +10 -10
- package/tests/utils.test.js +543 -1
- package/types/utils.d.ts +103 -0
package/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Lightweight modern frontend library — jQuery-like selectors, reactive
|
|
5
5
|
* components, SPA router, state management, HTTP client & utilities.
|
|
6
6
|
*
|
|
7
|
-
* @version 0.9.
|
|
7
|
+
* @version 0.9.6
|
|
8
8
|
* @license MIT
|
|
9
9
|
* @see https://z-query.com/docs
|
|
10
10
|
*/
|
|
@@ -74,6 +74,7 @@ export {
|
|
|
74
74
|
once,
|
|
75
75
|
sleep,
|
|
76
76
|
escapeHtml,
|
|
77
|
+
stripHtml,
|
|
77
78
|
html,
|
|
78
79
|
TrustedHTML,
|
|
79
80
|
trust,
|
|
@@ -90,6 +91,23 @@ export {
|
|
|
90
91
|
session,
|
|
91
92
|
EventBus,
|
|
92
93
|
bus,
|
|
94
|
+
range,
|
|
95
|
+
unique,
|
|
96
|
+
chunk,
|
|
97
|
+
groupBy,
|
|
98
|
+
pick,
|
|
99
|
+
omit,
|
|
100
|
+
getPath,
|
|
101
|
+
setPath,
|
|
102
|
+
isEmpty,
|
|
103
|
+
capitalize,
|
|
104
|
+
truncate,
|
|
105
|
+
clamp,
|
|
106
|
+
MemoizedFunction,
|
|
107
|
+
memoize,
|
|
108
|
+
RetryOptions,
|
|
109
|
+
retry,
|
|
110
|
+
timeout,
|
|
93
111
|
} from './types/utils';
|
|
94
112
|
|
|
95
113
|
export {
|
|
@@ -122,9 +140,13 @@ import type { createStore, getStore } from './types/store';
|
|
|
122
140
|
import type { HttpClient } from './types/http';
|
|
123
141
|
import type {
|
|
124
142
|
debounce, throttle, pipe, once, sleep,
|
|
125
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
143
|
+
escapeHtml, stripHtml, html, trust, uuid, camelCase, kebabCase,
|
|
126
144
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
127
145
|
StorageWrapper, EventBus,
|
|
146
|
+
range, unique, chunk, groupBy,
|
|
147
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
148
|
+
capitalize, truncate, clamp,
|
|
149
|
+
MemoizedFunction, memoize, RetryOptions, retry, timeout,
|
|
128
150
|
} from './types/utils';
|
|
129
151
|
import type { onError, ZQueryError, ErrorCode, guardCallback, validate } from './types/errors';
|
|
130
152
|
import type { morph, morphElement, safeEval } from './types/misc';
|
|
@@ -169,6 +191,10 @@ interface ZQueryStatic {
|
|
|
169
191
|
name(name: string): ZQueryCollection;
|
|
170
192
|
/** Children of `#parentId` as `ZQueryCollection`. */
|
|
171
193
|
children(parentId: string): ZQueryCollection;
|
|
194
|
+
/** `document.querySelector(selector)` — raw Element or null. */
|
|
195
|
+
qs(selector: string, context?: Element | Document): Element | null;
|
|
196
|
+
/** `document.querySelectorAll(selector)` — as a real `Array<Element>`. */
|
|
197
|
+
qsa(selector: string, context?: Element | Document): Element[];
|
|
172
198
|
|
|
173
199
|
// -- Static helpers ------------------------------------------------------
|
|
174
200
|
/**
|
|
@@ -258,6 +284,7 @@ interface ZQueryStatic {
|
|
|
258
284
|
sleep: typeof sleep;
|
|
259
285
|
|
|
260
286
|
escapeHtml: typeof escapeHtml;
|
|
287
|
+
stripHtml: typeof stripHtml;
|
|
261
288
|
html: typeof html;
|
|
262
289
|
trust: typeof trust;
|
|
263
290
|
uuid: typeof uuid;
|
|
@@ -273,8 +300,25 @@ interface ZQueryStatic {
|
|
|
273
300
|
|
|
274
301
|
storage: StorageWrapper;
|
|
275
302
|
session: StorageWrapper;
|
|
303
|
+
EventBus: typeof EventBus;
|
|
276
304
|
bus: EventBus;
|
|
277
305
|
|
|
306
|
+
range: typeof range;
|
|
307
|
+
unique: typeof unique;
|
|
308
|
+
chunk: typeof chunk;
|
|
309
|
+
groupBy: typeof groupBy;
|
|
310
|
+
pick: typeof pick;
|
|
311
|
+
omit: typeof omit;
|
|
312
|
+
getPath: typeof getPath;
|
|
313
|
+
setPath: typeof setPath;
|
|
314
|
+
isEmpty: typeof isEmpty;
|
|
315
|
+
capitalize: typeof capitalize;
|
|
316
|
+
truncate: typeof truncate;
|
|
317
|
+
clamp: typeof clamp;
|
|
318
|
+
memoize: typeof memoize;
|
|
319
|
+
retry: typeof retry;
|
|
320
|
+
timeout: typeof timeout;
|
|
321
|
+
|
|
278
322
|
// -- Meta ----------------------------------------------------------------
|
|
279
323
|
/** Library version string. */
|
|
280
324
|
version: string;
|
package/index.js
CHANGED
|
@@ -19,9 +19,13 @@ import { morph, morphElement } from './src/diff.js';
|
|
|
19
19
|
import { safeEval } from './src/expression.js';
|
|
20
20
|
import {
|
|
21
21
|
debounce, throttle, pipe, once, sleep,
|
|
22
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
22
|
+
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
23
23
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
24
|
-
storage, session, bus,
|
|
24
|
+
storage, session, EventBus, bus,
|
|
25
|
+
range, unique, chunk, groupBy,
|
|
26
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
27
|
+
capitalize, truncate, clamp,
|
|
28
|
+
memoize, retry, timeout,
|
|
25
29
|
} from './src/utils.js';
|
|
26
30
|
import { ZQueryError, ErrorCode, onError, reportError, guardCallback, validate } from './src/errors.js';
|
|
27
31
|
|
|
@@ -61,6 +65,8 @@ Object.defineProperty($, 'name', {
|
|
|
61
65
|
value: query.name, writable: true, configurable: true
|
|
62
66
|
});
|
|
63
67
|
$.children = query.children;
|
|
68
|
+
$.qs = query.qs;
|
|
69
|
+
$.qsa = query.qsa;
|
|
64
70
|
|
|
65
71
|
// --- Collection selector ---------------------------------------------------
|
|
66
72
|
/**
|
|
@@ -129,8 +135,10 @@ $.pipe = pipe;
|
|
|
129
135
|
$.once = once;
|
|
130
136
|
$.sleep = sleep;
|
|
131
137
|
$.escapeHtml = escapeHtml;
|
|
138
|
+
$.stripHtml = stripHtml;
|
|
132
139
|
$.html = html;
|
|
133
140
|
$.trust = trust;
|
|
141
|
+
$.TrustedHTML = TrustedHTML;
|
|
134
142
|
$.uuid = uuid;
|
|
135
143
|
$.camelCase = camelCase;
|
|
136
144
|
$.kebabCase = kebabCase;
|
|
@@ -141,7 +149,23 @@ $.param = param;
|
|
|
141
149
|
$.parseQuery = parseQuery;
|
|
142
150
|
$.storage = storage;
|
|
143
151
|
$.session = session;
|
|
152
|
+
$.EventBus = EventBus;
|
|
144
153
|
$.bus = bus;
|
|
154
|
+
$.range = range;
|
|
155
|
+
$.unique = unique;
|
|
156
|
+
$.chunk = chunk;
|
|
157
|
+
$.groupBy = groupBy;
|
|
158
|
+
$.pick = pick;
|
|
159
|
+
$.omit = omit;
|
|
160
|
+
$.getPath = getPath;
|
|
161
|
+
$.setPath = setPath;
|
|
162
|
+
$.isEmpty = isEmpty;
|
|
163
|
+
$.capitalize = capitalize;
|
|
164
|
+
$.truncate = truncate;
|
|
165
|
+
$.clamp = clamp;
|
|
166
|
+
$.memoize = memoize;
|
|
167
|
+
$.retry = retry;
|
|
168
|
+
$.timeout = timeout;
|
|
145
169
|
|
|
146
170
|
// --- Error handling --------------------------------------------------------
|
|
147
171
|
$.onError = onError;
|
|
@@ -189,9 +213,13 @@ export {
|
|
|
189
213
|
http,
|
|
190
214
|
ZQueryError, ErrorCode, onError, reportError, guardCallback, validate,
|
|
191
215
|
debounce, throttle, pipe, once, sleep,
|
|
192
|
-
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
216
|
+
escapeHtml, stripHtml, html, trust, TrustedHTML, uuid, camelCase, kebabCase,
|
|
193
217
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
194
|
-
storage, session, bus,
|
|
218
|
+
storage, session, EventBus, bus,
|
|
219
|
+
range, unique, chunk, groupBy,
|
|
220
|
+
pick, omit, getPath, setPath, isEmpty,
|
|
221
|
+
capitalize, truncate, clamp,
|
|
222
|
+
memoize, retry, timeout,
|
|
195
223
|
};
|
|
196
224
|
|
|
197
225
|
export default $;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Lightweight modern frontend library — jQuery-like selectors, reactive components, SPA router, and state management with zero dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/component.js
CHANGED
|
@@ -198,7 +198,7 @@ class Component {
|
|
|
198
198
|
const defaultSlotNodes = [];
|
|
199
199
|
[...el.childNodes].forEach(node => {
|
|
200
200
|
if (node.nodeType === 1 && node.hasAttribute('slot')) {
|
|
201
|
-
const slotName = node.getAttribute('slot');
|
|
201
|
+
const slotName = node.getAttribute('slot') || 'default';
|
|
202
202
|
if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
|
|
203
203
|
this._slotContent[slotName] += node.outerHTML;
|
|
204
204
|
} else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
|
|
@@ -607,6 +607,24 @@ class Component {
|
|
|
607
607
|
for (const [event, bindings] of eventMap) {
|
|
608
608
|
this._attachDelegatedEvent(event, bindings);
|
|
609
609
|
}
|
|
610
|
+
|
|
611
|
+
// .outside — attach a document-level listener for bindings that need
|
|
612
|
+
// to detect clicks/events outside their element.
|
|
613
|
+
this._outsideListeners = this._outsideListeners || [];
|
|
614
|
+
for (const [event, bindings] of eventMap) {
|
|
615
|
+
for (const binding of bindings) {
|
|
616
|
+
if (!binding.modifiers.includes('outside')) continue;
|
|
617
|
+
const outsideHandler = (e) => {
|
|
618
|
+
if (binding.el.contains(e.target)) return;
|
|
619
|
+
const match = binding.methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
|
|
620
|
+
if (!match) return;
|
|
621
|
+
const fn = this[match[1]];
|
|
622
|
+
if (typeof fn === 'function') fn.call(this, e);
|
|
623
|
+
};
|
|
624
|
+
document.addEventListener(event, outsideHandler, true);
|
|
625
|
+
this._outsideListeners.push({ event, handler: outsideHandler });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
610
628
|
}
|
|
611
629
|
|
|
612
630
|
// Attach a single delegated listener for an event type
|
|
@@ -620,15 +638,66 @@ class Component {
|
|
|
620
638
|
const handler = (e) => {
|
|
621
639
|
// Read bindings from live map — always up to date after re-renders
|
|
622
640
|
const currentBindings = this._eventBindings?.get(event) || [];
|
|
623
|
-
|
|
624
|
-
|
|
641
|
+
|
|
642
|
+
// Collect matching bindings with their matched elements, then sort
|
|
643
|
+
// deepest-first so .stop correctly prevents ancestor handlers
|
|
644
|
+
// (mimics real DOM bubbling order within delegated events).
|
|
645
|
+
const hits = [];
|
|
646
|
+
for (const binding of currentBindings) {
|
|
647
|
+
const matched = e.target.closest(binding.selector);
|
|
648
|
+
if (!matched) continue;
|
|
649
|
+
hits.push({ ...binding, matched });
|
|
650
|
+
}
|
|
651
|
+
hits.sort((a, b) => {
|
|
652
|
+
if (a.matched === b.matched) return 0;
|
|
653
|
+
return a.matched.contains(b.matched) ? 1 : -1;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
let stoppedAt = null; // Track elements that called .stop
|
|
657
|
+
for (const { selector, methodExpr, modifiers, el, matched } of hits) {
|
|
658
|
+
|
|
659
|
+
// In delegated events, .stop should prevent ancestor bindings from
|
|
660
|
+
// firing — stopPropagation alone only stops real DOM bubbling.
|
|
661
|
+
if (stoppedAt) {
|
|
662
|
+
let blocked = false;
|
|
663
|
+
for (const stopped of stoppedAt) {
|
|
664
|
+
if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
|
|
665
|
+
}
|
|
666
|
+
if (blocked) continue;
|
|
667
|
+
}
|
|
625
668
|
|
|
626
669
|
// .self — only fire if target is the element itself
|
|
627
670
|
if (modifiers.includes('self') && e.target !== el) continue;
|
|
628
671
|
|
|
672
|
+
// .outside — only fire if event target is OUTSIDE the element
|
|
673
|
+
if (modifiers.includes('outside')) {
|
|
674
|
+
if (el.contains(e.target)) continue;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Key modifiers — filter keyboard events by key
|
|
678
|
+
const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
|
|
679
|
+
let keyFiltered = false;
|
|
680
|
+
for (const mod of modifiers) {
|
|
681
|
+
if (_keyMap[mod]) {
|
|
682
|
+
const keys = _keyMap[mod].split('|');
|
|
683
|
+
if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (keyFiltered) continue;
|
|
687
|
+
|
|
688
|
+
// System key modifiers — require modifier keys to be held
|
|
689
|
+
if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
|
|
690
|
+
if (modifiers.includes('shift') && !e.shiftKey) continue;
|
|
691
|
+
if (modifiers.includes('alt') && !e.altKey) continue;
|
|
692
|
+
if (modifiers.includes('meta') && !e.metaKey) continue;
|
|
693
|
+
|
|
629
694
|
// Handle modifiers
|
|
630
695
|
if (modifiers.includes('prevent')) e.preventDefault();
|
|
631
|
-
if (modifiers.includes('stop'))
|
|
696
|
+
if (modifiers.includes('stop')) {
|
|
697
|
+
e.stopPropagation();
|
|
698
|
+
if (!stoppedAt) stoppedAt = [];
|
|
699
|
+
stoppedAt.push(matched);
|
|
700
|
+
}
|
|
632
701
|
|
|
633
702
|
// Build the invocation function
|
|
634
703
|
const invoke = (evt) => {
|
|
@@ -707,9 +776,12 @@ class Component {
|
|
|
707
776
|
// textarea, select (single & multiple), contenteditable
|
|
708
777
|
// Nested state keys: z-model="user.name" → this.state.user.name
|
|
709
778
|
// Modifiers (boolean attributes on the same element):
|
|
710
|
-
// z-lazy
|
|
711
|
-
// z-trim
|
|
712
|
-
// z-number
|
|
779
|
+
// z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
|
|
780
|
+
// z-trim — trim whitespace before writing to state
|
|
781
|
+
// z-number — force Number() conversion regardless of input type
|
|
782
|
+
// z-debounce — debounce state writes (default 250ms, or z-debounce="300")
|
|
783
|
+
// z-uppercase — convert string to uppercase before writing to state
|
|
784
|
+
// z-lowercase — convert string to lowercase before writing to state
|
|
713
785
|
//
|
|
714
786
|
// Writes to reactive state so the rest of the UI stays in sync.
|
|
715
787
|
// Focus and cursor position are preserved in _render() via focusInfo.
|
|
@@ -725,6 +797,10 @@ class Component {
|
|
|
725
797
|
const isLazy = el.hasAttribute('z-lazy');
|
|
726
798
|
const isTrim = el.hasAttribute('z-trim');
|
|
727
799
|
const isNum = el.hasAttribute('z-number');
|
|
800
|
+
const isUpper = el.hasAttribute('z-uppercase');
|
|
801
|
+
const isLower = el.hasAttribute('z-lowercase');
|
|
802
|
+
const hasDebounce = el.hasAttribute('z-debounce');
|
|
803
|
+
const debounceMs = hasDebounce ? (parseInt(el.getAttribute('z-debounce'), 10) || 250) : 0;
|
|
728
804
|
|
|
729
805
|
// Read current state value (supports dot-path keys)
|
|
730
806
|
const currentVal = _getPath(this.state, key);
|
|
@@ -765,6 +841,8 @@ class Component {
|
|
|
765
841
|
|
|
766
842
|
// Apply modifiers
|
|
767
843
|
if (isTrim && typeof val === 'string') val = val.trim();
|
|
844
|
+
if (isUpper && typeof val === 'string') val = val.toUpperCase();
|
|
845
|
+
if (isLower && typeof val === 'string') val = val.toLowerCase();
|
|
768
846
|
if (isNum || type === 'number' || type === 'range') val = Number(val);
|
|
769
847
|
|
|
770
848
|
// Write through the reactive proxy (triggers re-render).
|
|
@@ -772,7 +850,15 @@ class Component {
|
|
|
772
850
|
_setPath(this.state, key, val);
|
|
773
851
|
};
|
|
774
852
|
|
|
775
|
-
|
|
853
|
+
if (hasDebounce) {
|
|
854
|
+
let timer = null;
|
|
855
|
+
el.addEventListener(event, () => {
|
|
856
|
+
clearTimeout(timer);
|
|
857
|
+
timer = setTimeout(handler, debounceMs);
|
|
858
|
+
});
|
|
859
|
+
} else {
|
|
860
|
+
el.addEventListener(event, handler);
|
|
861
|
+
}
|
|
776
862
|
});
|
|
777
863
|
}
|
|
778
864
|
|
|
@@ -1058,6 +1144,10 @@ class Component {
|
|
|
1058
1144
|
}
|
|
1059
1145
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
1060
1146
|
this._listeners = [];
|
|
1147
|
+
if (this._outsideListeners) {
|
|
1148
|
+
this._outsideListeners.forEach(({ event, handler }) => document.removeEventListener(event, handler, true));
|
|
1149
|
+
this._outsideListeners = [];
|
|
1150
|
+
}
|
|
1061
1151
|
this._delegatedEvents = null;
|
|
1062
1152
|
this._eventBindings = null;
|
|
1063
1153
|
// Clear any pending debounce/throttle timers to prevent stale closures.
|
package/src/core.js
CHANGED
|
@@ -863,6 +863,8 @@ query.children = (parentId) => {
|
|
|
863
863
|
const p = document.getElementById(parentId);
|
|
864
864
|
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
865
865
|
};
|
|
866
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
867
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
866
868
|
|
|
867
869
|
// Create element shorthand — returns ZQueryCollection for chaining
|
|
868
870
|
query.create = (tag, attrs = {}, ...children) => {
|
|
@@ -870,7 +872,7 @@ query.create = (tag, attrs = {}, ...children) => {
|
|
|
870
872
|
for (const [k, v] of Object.entries(attrs)) {
|
|
871
873
|
if (k === 'class') el.className = v;
|
|
872
874
|
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
873
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
|
|
875
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
874
876
|
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
875
877
|
else el.setAttribute(k, v);
|
|
876
878
|
}
|
package/src/expression.js
CHANGED
|
@@ -169,6 +169,11 @@ function tokenize(expr) {
|
|
|
169
169
|
tokens.push({ t: T.OP, v: ch });
|
|
170
170
|
i++; continue;
|
|
171
171
|
}
|
|
172
|
+
// Spread operator: ...
|
|
173
|
+
if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
174
|
+
tokens.push({ t: T.OP, v: '...' });
|
|
175
|
+
i += 3; continue;
|
|
176
|
+
}
|
|
172
177
|
if ('()[]{},.?:'.includes(ch)) {
|
|
173
178
|
tokens.push({ t: T.PUNC, v: ch });
|
|
174
179
|
i++; continue;
|
|
@@ -232,7 +237,7 @@ class Parser {
|
|
|
232
237
|
this.next(); // consume ?
|
|
233
238
|
const truthy = this.parseExpression(0);
|
|
234
239
|
this.expect(T.PUNC, ':');
|
|
235
|
-
const falsy = this.parseExpression(
|
|
240
|
+
const falsy = this.parseExpression(0);
|
|
236
241
|
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
237
242
|
continue;
|
|
238
243
|
}
|
|
@@ -373,7 +378,12 @@ class Parser {
|
|
|
373
378
|
this.expect(T.PUNC, '(');
|
|
374
379
|
const args = [];
|
|
375
380
|
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
376
|
-
|
|
381
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
382
|
+
this.next();
|
|
383
|
+
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
384
|
+
} else {
|
|
385
|
+
args.push(this.parseExpression(0));
|
|
386
|
+
}
|
|
377
387
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
378
388
|
}
|
|
379
389
|
this.expect(T.PUNC, ')');
|
|
@@ -449,7 +459,12 @@ class Parser {
|
|
|
449
459
|
this.next();
|
|
450
460
|
const elements = [];
|
|
451
461
|
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
452
|
-
|
|
462
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
463
|
+
this.next();
|
|
464
|
+
elements.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
465
|
+
} else {
|
|
466
|
+
elements.push(this.parseExpression(0));
|
|
467
|
+
}
|
|
453
468
|
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
454
469
|
}
|
|
455
470
|
this.expect(T.PUNC, ']');
|
|
@@ -461,6 +476,14 @@ class Parser {
|
|
|
461
476
|
this.next();
|
|
462
477
|
const properties = [];
|
|
463
478
|
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
479
|
+
// Spread in object: { ...obj }
|
|
480
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
481
|
+
this.next();
|
|
482
|
+
properties.push({ spread: true, value: this.parseExpression(0) });
|
|
483
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
464
487
|
const keyTok = this.next();
|
|
465
488
|
let key;
|
|
466
489
|
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
@@ -492,7 +515,13 @@ class Parser {
|
|
|
492
515
|
|
|
493
516
|
// new keyword
|
|
494
517
|
if (tok.v === 'new') {
|
|
495
|
-
|
|
518
|
+
let classExpr = this.parsePrimary();
|
|
519
|
+
// Handle member access (e.g. ns.MyClass) without consuming call args
|
|
520
|
+
while (this.peek().t === T.PUNC && this.peek().v === '.') {
|
|
521
|
+
this.next();
|
|
522
|
+
const prop = this.next();
|
|
523
|
+
classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
|
|
524
|
+
}
|
|
496
525
|
let args = [];
|
|
497
526
|
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
498
527
|
args = this._parseArgs();
|
|
@@ -604,6 +633,12 @@ function evaluate(node, scope) {
|
|
|
604
633
|
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
605
634
|
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
606
635
|
if (name === 'console') return console;
|
|
636
|
+
if (name === 'Map') return Map;
|
|
637
|
+
if (name === 'Set') return Set;
|
|
638
|
+
if (name === 'RegExp') return RegExp;
|
|
639
|
+
if (name === 'Error') return Error;
|
|
640
|
+
if (name === 'URL') return URL;
|
|
641
|
+
if (name === 'URLSearchParams') return URLSearchParams;
|
|
607
642
|
return undefined;
|
|
608
643
|
}
|
|
609
644
|
|
|
@@ -642,10 +677,21 @@ function evaluate(node, scope) {
|
|
|
642
677
|
}
|
|
643
678
|
|
|
644
679
|
case 'optional_call': {
|
|
645
|
-
const
|
|
680
|
+
const calleeNode = node.callee;
|
|
681
|
+
const args = _evalArgs(node.args, scope);
|
|
682
|
+
// Method call: obj?.method() — bind `this` to obj
|
|
683
|
+
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
684
|
+
const obj = evaluate(calleeNode.obj, scope);
|
|
685
|
+
if (obj == null) return undefined;
|
|
686
|
+
const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
|
|
687
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
688
|
+
const fn = obj[prop];
|
|
689
|
+
if (typeof fn !== 'function') return undefined;
|
|
690
|
+
return fn.apply(obj, args);
|
|
691
|
+
}
|
|
692
|
+
const callee = evaluate(calleeNode, scope);
|
|
646
693
|
if (callee == null) return undefined;
|
|
647
694
|
if (typeof callee !== 'function') return undefined;
|
|
648
|
-
const args = node.args.map(a => evaluate(a, scope));
|
|
649
695
|
return callee(...args);
|
|
650
696
|
}
|
|
651
697
|
|
|
@@ -655,7 +701,7 @@ function evaluate(node, scope) {
|
|
|
655
701
|
// Only allow safe constructors
|
|
656
702
|
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
657
703
|
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
658
|
-
const args = node.args
|
|
704
|
+
const args = _evalArgs(node.args, scope);
|
|
659
705
|
return new Ctor(...args);
|
|
660
706
|
}
|
|
661
707
|
return undefined;
|
|
@@ -685,13 +731,32 @@ function evaluate(node, scope) {
|
|
|
685
731
|
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
686
732
|
}
|
|
687
733
|
|
|
688
|
-
case 'array':
|
|
689
|
-
|
|
734
|
+
case 'array': {
|
|
735
|
+
const arr = [];
|
|
736
|
+
for (const e of node.elements) {
|
|
737
|
+
if (e.type === 'spread') {
|
|
738
|
+
const iterable = evaluate(e.arg, scope);
|
|
739
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
740
|
+
for (const v of iterable) arr.push(v);
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
arr.push(evaluate(e, scope));
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return arr;
|
|
747
|
+
}
|
|
690
748
|
|
|
691
749
|
case 'object': {
|
|
692
750
|
const obj = {};
|
|
693
|
-
for (const
|
|
694
|
-
|
|
751
|
+
for (const prop of node.properties) {
|
|
752
|
+
if (prop.spread) {
|
|
753
|
+
const source = evaluate(prop.value, scope);
|
|
754
|
+
if (source != null && typeof source === 'object') {
|
|
755
|
+
Object.assign(obj, source);
|
|
756
|
+
}
|
|
757
|
+
} else {
|
|
758
|
+
obj[prop.key] = evaluate(prop.value, scope);
|
|
759
|
+
}
|
|
695
760
|
}
|
|
696
761
|
return obj;
|
|
697
762
|
}
|
|
@@ -712,12 +777,30 @@ function evaluate(node, scope) {
|
|
|
712
777
|
}
|
|
713
778
|
}
|
|
714
779
|
|
|
780
|
+
/**
|
|
781
|
+
* Evaluate a list of argument AST nodes, flattening any spread elements.
|
|
782
|
+
*/
|
|
783
|
+
function _evalArgs(argNodes, scope) {
|
|
784
|
+
const result = [];
|
|
785
|
+
for (const a of argNodes) {
|
|
786
|
+
if (a.type === 'spread') {
|
|
787
|
+
const iterable = evaluate(a.arg, scope);
|
|
788
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
789
|
+
for (const v of iterable) result.push(v);
|
|
790
|
+
}
|
|
791
|
+
} else {
|
|
792
|
+
result.push(evaluate(a, scope));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return result;
|
|
796
|
+
}
|
|
797
|
+
|
|
715
798
|
/**
|
|
716
799
|
* Resolve and execute a function call safely.
|
|
717
800
|
*/
|
|
718
801
|
function _resolveCall(node, scope) {
|
|
719
802
|
const callee = node.callee;
|
|
720
|
-
const args = node.args
|
|
803
|
+
const args = _evalArgs(node.args, scope);
|
|
721
804
|
|
|
722
805
|
// Method call: obj.method() — bind `this` to obj
|
|
723
806
|
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
@@ -791,8 +874,9 @@ function _evalBinary(node, scope) {
|
|
|
791
874
|
* @returns {*} — evaluation result, or undefined on error
|
|
792
875
|
*/
|
|
793
876
|
|
|
794
|
-
// AST cache — avoids re-tokenizing and re-parsing the same expression
|
|
795
|
-
//
|
|
877
|
+
// AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
|
|
878
|
+
// Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
|
|
879
|
+
// Eviction removes the least-recently-used (first) entry when at capacity.
|
|
796
880
|
const _astCache = new Map();
|
|
797
881
|
const _AST_CACHE_MAX = 512;
|
|
798
882
|
|
|
@@ -812,9 +896,12 @@ export function safeEval(expr, scope) {
|
|
|
812
896
|
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
813
897
|
}
|
|
814
898
|
|
|
815
|
-
// Check AST cache
|
|
899
|
+
// Check AST cache (LRU: move to end on hit)
|
|
816
900
|
let ast = _astCache.get(trimmed);
|
|
817
|
-
if (
|
|
901
|
+
if (ast) {
|
|
902
|
+
_astCache.delete(trimmed);
|
|
903
|
+
_astCache.set(trimmed, ast);
|
|
904
|
+
} else {
|
|
818
905
|
const tokens = tokenize(trimmed);
|
|
819
906
|
const parser = new Parser(tokens, scope);
|
|
820
907
|
ast = parser.parse();
|
package/src/http.js
CHANGED
|
@@ -84,8 +84,9 @@ async function request(method, url, data, options = {}) {
|
|
|
84
84
|
} else {
|
|
85
85
|
fetchOpts.signal = controller.signal;
|
|
86
86
|
}
|
|
87
|
+
let _timedOut = false;
|
|
87
88
|
if (timeout > 0) {
|
|
88
|
-
timer = setTimeout(() => controller.abort(), timeout);
|
|
89
|
+
timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// Run request interceptors
|
|
@@ -145,7 +146,10 @@ async function request(method, url, data, options = {}) {
|
|
|
145
146
|
} catch (err) {
|
|
146
147
|
if (timer) clearTimeout(timer);
|
|
147
148
|
if (err.name === 'AbortError') {
|
|
148
|
-
|
|
149
|
+
if (_timedOut) {
|
|
150
|
+
throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`Request aborted: ${method} ${fullURL}`);
|
|
149
153
|
}
|
|
150
154
|
throw err;
|
|
151
155
|
}
|
package/src/router.js
CHANGED
|
@@ -583,7 +583,12 @@ class Router {
|
|
|
583
583
|
if (typeof matched.component === 'string') {
|
|
584
584
|
const container = document.createElement(matched.component);
|
|
585
585
|
this._el.appendChild(container);
|
|
586
|
-
|
|
586
|
+
try {
|
|
587
|
+
this._instance = mount(container, matched.component, props);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
587
592
|
if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
|
|
588
593
|
}
|
|
589
594
|
// If component is a render function
|