zero-query 0.5.2 → 0.7.5
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 +12 -10
- package/cli/commands/build.js +7 -5
- package/cli/commands/bundle.js +286 -8
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +366 -0
- package/cli/commands/dev/server.js +158 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +147 -0
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/index.html +1 -0
- package/cli/scaffold/scripts/app.js +15 -22
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
- package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
- package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
- package/cli/scaffold/scripts/components/counter.js +30 -10
- package/cli/scaffold/scripts/components/home.js +3 -3
- package/cli/scaffold/scripts/components/todos.js +6 -5
- package/cli/scaffold/styles/styles.css +1 -0
- package/cli/utils.js +111 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +2005 -216
- package/dist/zquery.min.js +3 -13
- package/index.d.ts +149 -1080
- package/index.js +18 -7
- package/package.json +9 -3
- package/src/component.js +186 -45
- package/src/core.js +327 -35
- package/src/diff.js +280 -0
- package/src/errors.js +155 -0
- package/src/expression.js +806 -0
- package/src/http.js +18 -10
- package/src/reactive.js +29 -4
- package/src/router.js +59 -6
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
- package/tests/component.test.js +304 -0
- package/tests/core.test.js +726 -0
- package/tests/diff.test.js +194 -0
- package/tests/errors.test.js +162 -0
- package/tests/expression.test.js +334 -0
- package/tests/http.test.js +181 -0
- package/tests/reactive.test.js +191 -0
- package/tests/router.test.js +332 -0
- package/tests/store.test.js +253 -0
- package/tests/utils.test.js +353 -0
- package/types/collection.d.ts +368 -0
- package/types/component.d.ts +210 -0
- package/types/errors.d.ts +103 -0
- package/types/http.d.ts +81 -0
- package/types/misc.d.ts +166 -0
- package/types/reactive.d.ts +76 -0
- package/types/router.d.ts +132 -0
- package/types/ssr.d.ts +49 -0
- package/types/store.d.ts +107 -0
- package/types/utils.d.ts +142 -0
- /package/cli/commands/{dev.js → dev.old.js} +0 -0
package/index.js
CHANGED
|
@@ -15,12 +15,15 @@ import { component, mount, mountAll, getInstance, destroy, getRegistry, style }
|
|
|
15
15
|
import { createRouter, getRouter } from './src/router.js';
|
|
16
16
|
import { createStore, getStore } from './src/store.js';
|
|
17
17
|
import { http } from './src/http.js';
|
|
18
|
+
import { morph } from './src/diff.js';
|
|
19
|
+
import { safeEval } from './src/expression.js';
|
|
18
20
|
import {
|
|
19
21
|
debounce, throttle, pipe, once, sleep,
|
|
20
22
|
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
21
23
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
|
22
24
|
storage, session, bus,
|
|
23
25
|
} from './src/utils.js';
|
|
26
|
+
import { ZQueryError, ErrorCode, onError, reportError } from './src/errors.js';
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
// ---------------------------------------------------------------------------
|
|
@@ -28,16 +31,16 @@ import {
|
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* Main selector function
|
|
34
|
+
* Main selector function — always returns a ZQueryCollection (like jQuery).
|
|
32
35
|
*
|
|
33
|
-
* $('selector') →
|
|
34
|
-
* $('<div>hello</div>') →
|
|
35
|
-
* $(element) →
|
|
36
|
+
* $('selector') → ZQueryCollection (querySelectorAll)
|
|
37
|
+
* $('<div>hello</div>') → ZQueryCollection from created elements
|
|
38
|
+
* $(element) → ZQueryCollection wrapping the element
|
|
36
39
|
* $(fn) → DOMContentLoaded shorthand
|
|
37
40
|
*
|
|
38
41
|
* @param {string|Element|NodeList|Function} selector
|
|
39
42
|
* @param {string|Element} [context]
|
|
40
|
-
* @returns {
|
|
43
|
+
* @returns {ZQueryCollection}
|
|
41
44
|
*/
|
|
42
45
|
function $(selector, context) {
|
|
43
46
|
// $(fn) → DOM ready shorthand
|
|
@@ -57,8 +60,6 @@ $.tag = query.tag;
|
|
|
57
60
|
Object.defineProperty($, 'name', {
|
|
58
61
|
value: query.name, writable: true, configurable: true
|
|
59
62
|
});
|
|
60
|
-
$.attr = query.attr;
|
|
61
|
-
$.data = query.data;
|
|
62
63
|
$.children = query.children;
|
|
63
64
|
|
|
64
65
|
// --- Collection selector ---------------------------------------------------
|
|
@@ -100,6 +101,8 @@ $.getInstance = getInstance;
|
|
|
100
101
|
$.destroy = destroy;
|
|
101
102
|
$.components = getRegistry;
|
|
102
103
|
$.style = style;
|
|
104
|
+
$.morph = morph;
|
|
105
|
+
$.safeEval = safeEval;
|
|
103
106
|
|
|
104
107
|
// --- Router ----------------------------------------------------------------
|
|
105
108
|
$.router = createRouter;
|
|
@@ -138,6 +141,11 @@ $.storage = storage;
|
|
|
138
141
|
$.session = session;
|
|
139
142
|
$.bus = bus;
|
|
140
143
|
|
|
144
|
+
// --- Error handling --------------------------------------------------------
|
|
145
|
+
$.onError = onError;
|
|
146
|
+
$.ZQueryError = ZQueryError;
|
|
147
|
+
$.ErrorCode = ErrorCode;
|
|
148
|
+
|
|
141
149
|
// --- Meta ------------------------------------------------------------------
|
|
142
150
|
$.version = '__VERSION__';
|
|
143
151
|
$.meta = {}; // populated at build time by CLI bundler
|
|
@@ -169,9 +177,12 @@ export {
|
|
|
169
177
|
queryAll,
|
|
170
178
|
reactive, Signal, signal, computed, effect,
|
|
171
179
|
component, mount, mountAll, getInstance, destroy, getRegistry, style,
|
|
180
|
+
morph,
|
|
181
|
+
safeEval,
|
|
172
182
|
createRouter, getRouter,
|
|
173
183
|
createStore, getStore,
|
|
174
184
|
http,
|
|
185
|
+
ZQueryError, ErrorCode, onError, reportError,
|
|
175
186
|
debounce, throttle, pipe, once, sleep,
|
|
176
187
|
escapeHtml, html, trust, uuid, camelCase, kebabCase,
|
|
177
188
|
deepClone, deepMerge, isEqual, param, parseQuery,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zero-query",
|
|
3
|
-
"version": "0.5
|
|
3
|
+
"version": "0.7.5",
|
|
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",
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src",
|
|
12
|
+
"tests",
|
|
12
13
|
"dist",
|
|
13
14
|
"cli",
|
|
15
|
+
"types",
|
|
14
16
|
"index.js",
|
|
15
17
|
"index.d.ts",
|
|
16
18
|
"LICENSE",
|
|
@@ -21,7 +23,9 @@
|
|
|
21
23
|
"dev": "node cli/index.js dev zquery-website",
|
|
22
24
|
"dev-lib": "node cli/index.js build --watch",
|
|
23
25
|
"bundle": "node cli/index.js bundle",
|
|
24
|
-
"bundle:app": "node cli/index.js bundle zquery-website --minimal"
|
|
26
|
+
"bundle:app": "node cli/index.js bundle zquery-website --minimal",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest"
|
|
25
29
|
},
|
|
26
30
|
"keywords": [
|
|
27
31
|
"dom",
|
|
@@ -50,7 +54,9 @@
|
|
|
50
54
|
"publishConfig": {
|
|
51
55
|
"access": "public"
|
|
52
56
|
},
|
|
53
|
-
"
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"jsdom": "^28.1.0",
|
|
59
|
+
"vitest": "^4.0.18",
|
|
54
60
|
"zero-http": "^0.2.3"
|
|
55
61
|
}
|
|
56
62
|
}
|
package/src/component.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { reactive } from './reactive.js';
|
|
23
|
+
import { morph } from './diff.js';
|
|
24
|
+
import { safeEval } from './expression.js';
|
|
25
|
+
import { reportError, ErrorCode, ZQueryError } from './errors.js';
|
|
23
26
|
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
25
28
|
// Component registry & external resource cache
|
|
@@ -195,10 +198,27 @@ class Component {
|
|
|
195
198
|
this._destroyed = false;
|
|
196
199
|
this._updateQueued = false;
|
|
197
200
|
this._listeners = [];
|
|
201
|
+
this._watchCleanups = [];
|
|
198
202
|
|
|
199
203
|
// Refs map
|
|
200
204
|
this.refs = {};
|
|
201
205
|
|
|
206
|
+
// Capture slot content before first render replaces it
|
|
207
|
+
this._slotContent = {};
|
|
208
|
+
const defaultSlotNodes = [];
|
|
209
|
+
[...el.childNodes].forEach(node => {
|
|
210
|
+
if (node.nodeType === 1 && node.hasAttribute('slot')) {
|
|
211
|
+
const slotName = node.getAttribute('slot');
|
|
212
|
+
if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
|
|
213
|
+
this._slotContent[slotName] += node.outerHTML;
|
|
214
|
+
} else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
|
|
215
|
+
defaultSlotNodes.push(node.nodeType === 1 ? node.outerHTML : node.textContent);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
if (defaultSlotNodes.length) {
|
|
219
|
+
this._slotContent['default'] = defaultSlotNodes.join('');
|
|
220
|
+
}
|
|
221
|
+
|
|
202
222
|
// Props (read-only from parent)
|
|
203
223
|
this.props = Object.freeze({ ...props });
|
|
204
224
|
|
|
@@ -207,10 +227,25 @@ class Component {
|
|
|
207
227
|
? definition.state()
|
|
208
228
|
: { ...(definition.state || {}) };
|
|
209
229
|
|
|
210
|
-
this.state = reactive(initialState, () => {
|
|
211
|
-
if (!this._destroyed)
|
|
230
|
+
this.state = reactive(initialState, (key, value, old) => {
|
|
231
|
+
if (!this._destroyed) {
|
|
232
|
+
// Run watchers for the changed key
|
|
233
|
+
this._runWatchers(key, value, old);
|
|
234
|
+
this._scheduleUpdate();
|
|
235
|
+
}
|
|
212
236
|
});
|
|
213
237
|
|
|
238
|
+
// Computed properties — lazy getters derived from state
|
|
239
|
+
this.computed = {};
|
|
240
|
+
if (definition.computed) {
|
|
241
|
+
for (const [name, fn] of Object.entries(definition.computed)) {
|
|
242
|
+
Object.defineProperty(this.computed, name, {
|
|
243
|
+
get: () => fn.call(this, this.state.__raw || this.state),
|
|
244
|
+
enumerable: true
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
214
249
|
// Bind all user methods to this instance
|
|
215
250
|
for (const [key, val] of Object.entries(definition)) {
|
|
216
251
|
if (typeof val === 'function' && !_reservedKeys.has(key)) {
|
|
@@ -219,7 +254,36 @@ class Component {
|
|
|
219
254
|
}
|
|
220
255
|
|
|
221
256
|
// Init lifecycle
|
|
222
|
-
if (definition.init)
|
|
257
|
+
if (definition.init) {
|
|
258
|
+
try { definition.init.call(this); }
|
|
259
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${definition._name}" init() threw`, { component: definition._name }, err); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Set up watchers after init so initial state is ready
|
|
263
|
+
if (definition.watch) {
|
|
264
|
+
this._prevWatchValues = {};
|
|
265
|
+
for (const key of Object.keys(definition.watch)) {
|
|
266
|
+
this._prevWatchValues[key] = _getPath(this.state.__raw || this.state, key);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Run registered watchers for a changed key
|
|
272
|
+
_runWatchers(changedKey, value, old) {
|
|
273
|
+
const watchers = this._def.watch;
|
|
274
|
+
if (!watchers) return;
|
|
275
|
+
for (const [key, handler] of Object.entries(watchers)) {
|
|
276
|
+
// Match exact key or parent key (e.g. watcher on 'user' fires when 'user.name' changes)
|
|
277
|
+
if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.') || changedKey === key) {
|
|
278
|
+
const currentVal = _getPath(this.state.__raw || this.state, key);
|
|
279
|
+
const prevVal = this._prevWatchValues?.[key];
|
|
280
|
+
if (currentVal !== prevVal) {
|
|
281
|
+
const fn = typeof handler === 'function' ? handler : handler.handler;
|
|
282
|
+
if (typeof fn === 'function') fn.call(this, currentVal, prevVal);
|
|
283
|
+
if (this._prevWatchValues) this._prevWatchValues[key] = currentVal;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
223
287
|
}
|
|
224
288
|
|
|
225
289
|
// Schedule a batched DOM update (microtask)
|
|
@@ -317,11 +381,14 @@ class Component {
|
|
|
317
381
|
if (def.styleUrl && !def._styleLoaded) {
|
|
318
382
|
const su = def.styleUrl;
|
|
319
383
|
if (typeof su === 'string') {
|
|
320
|
-
|
|
384
|
+
const resolved = _resolveUrl(su, base);
|
|
385
|
+
def._externalStyles = await _fetchResource(resolved);
|
|
386
|
+
def._resolvedStyleUrls = [resolved];
|
|
321
387
|
} else if (Array.isArray(su)) {
|
|
322
388
|
const urls = su.map(u => _resolveUrl(u, base));
|
|
323
389
|
const results = await Promise.all(urls.map(u => _fetchResource(u)));
|
|
324
390
|
def._externalStyles = results.join('\n');
|
|
391
|
+
def._resolvedStyleUrls = urls;
|
|
325
392
|
}
|
|
326
393
|
def._styleLoaded = true;
|
|
327
394
|
}
|
|
@@ -394,17 +461,29 @@ class Component {
|
|
|
394
461
|
// Then do global {{expression}} interpolation on the remaining content
|
|
395
462
|
html = html.replace(/\{\{(.+?)\}\}/g, (_, expr) => {
|
|
396
463
|
try {
|
|
397
|
-
|
|
464
|
+
const result = safeEval(expr.trim(), [
|
|
398
465
|
this.state.__raw || this.state,
|
|
399
|
-
this.props,
|
|
400
|
-
|
|
401
|
-
|
|
466
|
+
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
467
|
+
]);
|
|
468
|
+
return result != null ? result : '';
|
|
402
469
|
} catch { return ''; }
|
|
403
470
|
});
|
|
404
471
|
} else {
|
|
405
472
|
html = '';
|
|
406
473
|
}
|
|
407
474
|
|
|
475
|
+
// -- Slot distribution ----------------------------------------
|
|
476
|
+
// Replace <slot> elements with captured slot content from parent.
|
|
477
|
+
// <slot> → default slot content
|
|
478
|
+
// <slot name="header"> → named slot content
|
|
479
|
+
// Fallback content between <slot>...</slot> used when no content provided.
|
|
480
|
+
if (html.includes('<slot')) {
|
|
481
|
+
html = html.replace(/<slot(?:\s+name="([^"]*)")?\s*(?:\/>|>([\s\S]*?)<\/slot>)/g, (_, name, fallback) => {
|
|
482
|
+
const slotName = name || 'default';
|
|
483
|
+
return this._slotContent[slotName] || fallback || '';
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
408
487
|
// Combine inline styles + external styles
|
|
409
488
|
const combinedStyles = [
|
|
410
489
|
this._def.styles || '',
|
|
@@ -415,33 +494,52 @@ class Component {
|
|
|
415
494
|
if (!this._mounted && combinedStyles) {
|
|
416
495
|
const scopeAttr = `z-s${this._uid}`;
|
|
417
496
|
this._el.setAttribute(scopeAttr, '');
|
|
418
|
-
|
|
497
|
+
let inAtBlock = 0;
|
|
498
|
+
const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
|
|
499
|
+
if (match === '}') {
|
|
500
|
+
if (inAtBlock > 0) inAtBlock--;
|
|
501
|
+
return match;
|
|
502
|
+
}
|
|
503
|
+
const trimmed = selector.trim();
|
|
504
|
+
// Don't scope @-rules (@media, @keyframes, @supports, @container, @layer, @font-face, etc.)
|
|
505
|
+
if (trimmed.startsWith('@')) {
|
|
506
|
+
inAtBlock++;
|
|
507
|
+
return match;
|
|
508
|
+
}
|
|
509
|
+
// Don't scope keyframe stops (from, to, 0%, 50%, etc.)
|
|
510
|
+
if (inAtBlock > 0 && /^[\d%\s,fromto]+$/.test(trimmed.replace(/\s/g, ''))) {
|
|
511
|
+
return match;
|
|
512
|
+
}
|
|
419
513
|
return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
|
|
420
514
|
});
|
|
421
515
|
const styleEl = document.createElement('style');
|
|
422
516
|
styleEl.textContent = scoped;
|
|
423
517
|
styleEl.setAttribute('data-zq-component', this._def._name || '');
|
|
518
|
+
styleEl.setAttribute('data-zq-scope', scopeAttr);
|
|
519
|
+
if (this._def._resolvedStyleUrls) {
|
|
520
|
+
styleEl.setAttribute('data-zq-style-urls', this._def._resolvedStyleUrls.join(' '));
|
|
521
|
+
if (this._def.styles) {
|
|
522
|
+
styleEl.setAttribute('data-zq-inline', this._def.styles);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
424
525
|
document.head.appendChild(styleEl);
|
|
425
526
|
this._styleEl = styleEl;
|
|
426
527
|
}
|
|
427
528
|
|
|
428
529
|
// -- Focus preservation ----------------------------------------
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
// input/textarea/select inside the component, not only z-model.
|
|
530
|
+
// DOM morphing preserves unchanged nodes naturally, but we still
|
|
531
|
+
// track focus for cases where the focused element's subtree changes.
|
|
432
532
|
let _focusInfo = null;
|
|
433
533
|
const _active = document.activeElement;
|
|
434
534
|
if (_active && this._el.contains(_active)) {
|
|
435
535
|
const modelKey = _active.getAttribute?.('z-model');
|
|
436
536
|
const refKey = _active.getAttribute?.('z-ref');
|
|
437
|
-
// Build a selector that can locate the same element after re-render
|
|
438
537
|
let selector = null;
|
|
439
538
|
if (modelKey) {
|
|
440
539
|
selector = `[z-model="${modelKey}"]`;
|
|
441
540
|
} else if (refKey) {
|
|
442
541
|
selector = `[z-ref="${refKey}"]`;
|
|
443
542
|
} else {
|
|
444
|
-
// Fallback: match by tag + type + name + placeholder combination
|
|
445
543
|
const tag = _active.tagName.toLowerCase();
|
|
446
544
|
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
447
545
|
let s = tag;
|
|
@@ -461,8 +559,13 @@ class Component {
|
|
|
461
559
|
}
|
|
462
560
|
}
|
|
463
561
|
|
|
464
|
-
// Update DOM
|
|
465
|
-
|
|
562
|
+
// Update DOM via morphing (diffing) — preserves unchanged nodes
|
|
563
|
+
// First render uses innerHTML for speed; subsequent renders morph.
|
|
564
|
+
if (!this._mounted) {
|
|
565
|
+
this._el.innerHTML = html;
|
|
566
|
+
} else {
|
|
567
|
+
morph(this._el, html);
|
|
568
|
+
}
|
|
466
569
|
|
|
467
570
|
// Process structural & attribute directives
|
|
468
571
|
this._processDirectives();
|
|
@@ -472,10 +575,10 @@ class Component {
|
|
|
472
575
|
this._bindRefs();
|
|
473
576
|
this._bindModels();
|
|
474
577
|
|
|
475
|
-
// Restore focus
|
|
578
|
+
// Restore focus if the morph replaced the focused element
|
|
476
579
|
if (_focusInfo) {
|
|
477
580
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
478
|
-
if (el) {
|
|
581
|
+
if (el && el !== document.activeElement) {
|
|
479
582
|
el.focus();
|
|
480
583
|
try {
|
|
481
584
|
if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
|
|
@@ -490,9 +593,15 @@ class Component {
|
|
|
490
593
|
|
|
491
594
|
if (!this._mounted) {
|
|
492
595
|
this._mounted = true;
|
|
493
|
-
if (this._def.mounted)
|
|
596
|
+
if (this._def.mounted) {
|
|
597
|
+
try { this._def.mounted.call(this); }
|
|
598
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" mounted() threw`, { component: this._def._name }, err); }
|
|
599
|
+
}
|
|
494
600
|
} else {
|
|
495
|
-
if (this._def.updated)
|
|
601
|
+
if (this._def.updated) {
|
|
602
|
+
try { this._def.updated.call(this); }
|
|
603
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" updated() threw`, { component: this._def._name }, err); }
|
|
604
|
+
}
|
|
496
605
|
}
|
|
497
606
|
}
|
|
498
607
|
|
|
@@ -701,18 +810,13 @@ class Component {
|
|
|
701
810
|
}
|
|
702
811
|
|
|
703
812
|
// ---------------------------------------------------------------------------
|
|
704
|
-
// Expression evaluator —
|
|
813
|
+
// Expression evaluator — CSP-safe parser (no eval / new Function)
|
|
705
814
|
// ---------------------------------------------------------------------------
|
|
706
815
|
_evalExpr(expr) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
this.props,
|
|
712
|
-
this.refs,
|
|
713
|
-
typeof window !== 'undefined' ? window.$ : undefined
|
|
714
|
-
);
|
|
715
|
-
} catch { return undefined; }
|
|
816
|
+
return safeEval(expr, [
|
|
817
|
+
this.state.__raw || this.state,
|
|
818
|
+
{ props: this.props, refs: this.refs, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
819
|
+
]);
|
|
716
820
|
}
|
|
717
821
|
|
|
718
822
|
// ---------------------------------------------------------------------------
|
|
@@ -774,13 +878,15 @@ class Component {
|
|
|
774
878
|
const evalReplace = (str, item, index) =>
|
|
775
879
|
str.replace(/\{\{(.+?)\}\}/g, (_, inner) => {
|
|
776
880
|
try {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
881
|
+
const loopScope = {};
|
|
882
|
+
loopScope[itemVar] = item;
|
|
883
|
+
loopScope[indexVar] = index;
|
|
884
|
+
const result = safeEval(inner.trim(), [
|
|
885
|
+
loopScope,
|
|
780
886
|
this.state.__raw || this.state,
|
|
781
|
-
this.props,
|
|
782
|
-
|
|
783
|
-
|
|
887
|
+
{ props: this.props, computed: this.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
888
|
+
]);
|
|
889
|
+
return result != null ? result : '';
|
|
784
890
|
} catch { return ''; }
|
|
785
891
|
});
|
|
786
892
|
|
|
@@ -945,7 +1051,10 @@ class Component {
|
|
|
945
1051
|
destroy() {
|
|
946
1052
|
if (this._destroyed) return;
|
|
947
1053
|
this._destroyed = true;
|
|
948
|
-
if (this._def.destroyed)
|
|
1054
|
+
if (this._def.destroyed) {
|
|
1055
|
+
try { this._def.destroyed.call(this); }
|
|
1056
|
+
catch (err) { reportError(ErrorCode.COMP_LIFECYCLE, `Component "${this._def._name}" destroyed() threw`, { component: this._def._name }, err); }
|
|
1057
|
+
}
|
|
949
1058
|
this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
|
|
950
1059
|
this._listeners = [];
|
|
951
1060
|
if (this._styleEl) this._styleEl.remove();
|
|
@@ -958,7 +1067,8 @@ class Component {
|
|
|
958
1067
|
// Reserved definition keys (not user methods)
|
|
959
1068
|
const _reservedKeys = new Set([
|
|
960
1069
|
'state', 'render', 'styles', 'init', 'mounted', 'updated', 'destroyed', 'props',
|
|
961
|
-
'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage', 'base'
|
|
1070
|
+
'templateUrl', 'styleUrl', 'templates', 'pages', 'activePage', 'base',
|
|
1071
|
+
'computed', 'watch'
|
|
962
1072
|
]);
|
|
963
1073
|
|
|
964
1074
|
|
|
@@ -972,8 +1082,11 @@ const _reservedKeys = new Set([
|
|
|
972
1082
|
* @param {object} definition — component definition
|
|
973
1083
|
*/
|
|
974
1084
|
export function component(name, definition) {
|
|
1085
|
+
if (!name || typeof name !== 'string') {
|
|
1086
|
+
throw new ZQueryError(ErrorCode.COMP_INVALID_NAME, 'Component name must be a non-empty string');
|
|
1087
|
+
}
|
|
975
1088
|
if (!name.includes('-')) {
|
|
976
|
-
throw new
|
|
1089
|
+
throw new ZQueryError(ErrorCode.COMP_INVALID_NAME, `Component name "${name}" must contain a hyphen (Web Component convention)`);
|
|
977
1090
|
}
|
|
978
1091
|
definition._name = name;
|
|
979
1092
|
|
|
@@ -998,10 +1111,10 @@ export function component(name, definition) {
|
|
|
998
1111
|
*/
|
|
999
1112
|
export function mount(target, componentName, props = {}) {
|
|
1000
1113
|
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
1001
|
-
if (!el) throw new
|
|
1114
|
+
if (!el) throw new ZQueryError(ErrorCode.COMP_MOUNT_TARGET, `Mount target "${target}" not found`, { target });
|
|
1002
1115
|
|
|
1003
1116
|
const def = _registry.get(componentName);
|
|
1004
|
-
if (!def) throw new
|
|
1117
|
+
if (!def) throw new ZQueryError(ErrorCode.COMP_NOT_FOUND, `Component "${componentName}" not registered`, { component: componentName });
|
|
1005
1118
|
|
|
1006
1119
|
// Destroy existing instance
|
|
1007
1120
|
if (_instances.has(el)) _instances.get(el).destroy();
|
|
@@ -1024,12 +1137,40 @@ export function mountAll(root = document.body) {
|
|
|
1024
1137
|
|
|
1025
1138
|
// Extract props from attributes
|
|
1026
1139
|
const props = {};
|
|
1140
|
+
|
|
1141
|
+
// Find parent component instance for evaluating dynamic prop expressions
|
|
1142
|
+
let parentInstance = null;
|
|
1143
|
+
let ancestor = tag.parentElement;
|
|
1144
|
+
while (ancestor) {
|
|
1145
|
+
if (_instances.has(ancestor)) {
|
|
1146
|
+
parentInstance = _instances.get(ancestor);
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
ancestor = ancestor.parentElement;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1027
1152
|
[...tag.attributes].forEach(attr => {
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1153
|
+
if (attr.name.startsWith('@') || attr.name.startsWith('z-')) return;
|
|
1154
|
+
|
|
1155
|
+
// Dynamic prop: :propName="expression" — evaluate in parent context
|
|
1156
|
+
if (attr.name.startsWith(':')) {
|
|
1157
|
+
const propName = attr.name.slice(1);
|
|
1158
|
+
if (parentInstance) {
|
|
1159
|
+
props[propName] = safeEval(attr.value, [
|
|
1160
|
+
parentInstance.state.__raw || parentInstance.state,
|
|
1161
|
+
{ props: parentInstance.props, refs: parentInstance.refs, computed: parentInstance.computed, $: typeof window !== 'undefined' ? window.$ : undefined }
|
|
1162
|
+
]);
|
|
1163
|
+
} else {
|
|
1164
|
+
// No parent — try JSON parse
|
|
1165
|
+
try { props[propName] = JSON.parse(attr.value); }
|
|
1166
|
+
catch { props[propName] = attr.value; }
|
|
1167
|
+
}
|
|
1168
|
+
return;
|
|
1032
1169
|
}
|
|
1170
|
+
|
|
1171
|
+
// Static prop
|
|
1172
|
+
try { props[attr.name] = JSON.parse(attr.value); }
|
|
1173
|
+
catch { props[attr.name] = attr.value; }
|
|
1033
1174
|
});
|
|
1034
1175
|
|
|
1035
1176
|
const instance = new Component(tag, def, props);
|