zero-query 0.9.9 → 1.0.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.
- package/README.md +34 -33
- package/cli/args.js +1 -1
- package/cli/commands/build.js +2 -2
- package/cli/commands/bundle.js +21 -18
- package/cli/commands/create.js +9 -2
- package/cli/commands/dev/devtools/index.js +1 -1
- package/cli/commands/dev/devtools/js/core.js +14 -14
- package/cli/commands/dev/devtools/js/elements.js +4 -4
- package/cli/commands/dev/devtools/js/stats.js +1 -1
- package/cli/commands/dev/devtools/styles.css +2 -2
- package/cli/commands/dev/index.js +2 -2
- package/cli/commands/dev/logger.js +1 -1
- package/cli/commands/dev/overlay.js +21 -14
- package/cli/commands/dev/server.js +5 -5
- package/cli/commands/dev/validator.js +7 -7
- package/cli/commands/dev/watcher.js +6 -6
- package/cli/help.js +3 -3
- package/cli/index.js +1 -1
- package/cli/scaffold/default/app/app.js +17 -18
- package/cli/scaffold/default/app/components/about.js +9 -9
- package/cli/scaffold/default/app/components/api-demo.js +6 -6
- package/cli/scaffold/default/app/components/contact-card.js +4 -4
- package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
- package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
- package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
- package/cli/scaffold/default/app/components/counter.js +8 -8
- package/cli/scaffold/default/app/components/home.js +13 -13
- package/cli/scaffold/default/app/components/not-found.js +1 -1
- package/cli/scaffold/default/app/components/playground/playground.css +1 -1
- package/cli/scaffold/default/app/components/playground/playground.html +11 -11
- package/cli/scaffold/default/app/components/playground/playground.js +11 -11
- package/cli/scaffold/default/app/components/todos.js +8 -8
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
- package/cli/scaffold/default/app/routes.js +1 -1
- package/cli/scaffold/default/app/store.js +1 -1
- package/cli/scaffold/default/global.css +2 -2
- package/cli/scaffold/default/index.html +2 -2
- package/cli/scaffold/minimal/app/app.js +6 -7
- package/cli/scaffold/minimal/app/components/about.js +5 -5
- package/cli/scaffold/minimal/app/components/counter.js +6 -6
- package/cli/scaffold/minimal/app/components/home.js +8 -8
- package/cli/scaffold/minimal/app/components/not-found.js +1 -1
- package/cli/scaffold/minimal/app/routes.js +1 -1
- package/cli/scaffold/minimal/app/store.js +1 -1
- package/cli/scaffold/minimal/global.css +2 -2
- package/cli/scaffold/minimal/index.html +1 -1
- package/cli/scaffold/ssr/app/app.js +1 -2
- package/cli/scaffold/ssr/app/components/about.js +5 -5
- package/cli/scaffold/ssr/app/components/home.js +2 -2
- package/cli/scaffold/ssr/app/components/not-found.js +2 -2
- package/cli/scaffold/ssr/app/routes.js +1 -1
- package/cli/scaffold/ssr/global.css +3 -4
- package/cli/scaffold/ssr/index.html +2 -2
- package/cli/scaffold/ssr/server/index.js +26 -25
- package/cli/utils.js +6 -6
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +508 -227
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +16 -13
- package/index.js +7 -5
- package/package.json +3 -3
- package/src/component.js +64 -63
- package/src/core.js +15 -15
- package/src/diff.js +38 -38
- package/src/errors.js +17 -17
- package/src/expression.js +15 -17
- package/src/http.js +4 -4
- package/src/reactive.js +75 -9
- package/src/router.js +104 -24
- package/src/ssr.js +28 -28
- package/src/store.js +103 -21
- package/src/utils.js +64 -12
- package/tests/audit.test.js +143 -15
- package/tests/cli.test.js +20 -20
- package/tests/component.test.js +121 -121
- package/tests/core.test.js +56 -56
- package/tests/diff.test.js +42 -42
- package/tests/errors.test.js +5 -5
- package/tests/expression.test.js +58 -53
- package/tests/http.test.js +20 -20
- package/tests/reactive.test.js +185 -24
- package/tests/router.test.js +501 -74
- package/tests/ssr.test.js +15 -13
- package/tests/store.test.js +264 -23
- package/tests/test-minifier.js +153 -0
- package/tests/test-ssr.js +27 -0
- package/tests/utils.test.js +163 -26
- package/types/collection.d.ts +2 -2
- package/types/component.d.ts +5 -5
- package/types/errors.d.ts +3 -3
- package/types/http.d.ts +3 -3
- package/types/misc.d.ts +9 -9
- package/types/reactive.d.ts +25 -3
- package/types/router.d.ts +10 -6
- package/types/ssr.d.ts +2 -2
- package/types/store.d.ts +40 -5
- package/types/utils.d.ts +1 -1
package/src/core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery Core
|
|
2
|
+
* zQuery Core - Selector engine & chainable DOM collection
|
|
3
3
|
*
|
|
4
4
|
* Extends the quick-ref pattern (Id, Class, Classes, Children)
|
|
5
5
|
* into a full jQuery-like chainable wrapper with modern APIs.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { morph as _morph, morphElement as _morphElement } from './diff.js';
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// ZQueryCollection
|
|
11
|
+
// ZQueryCollection - wraps an array of elements with chainable methods
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
export class ZQueryCollection {
|
|
14
14
|
constructor(elements) {
|
|
@@ -228,7 +228,7 @@ export class ZQueryCollection {
|
|
|
228
228
|
// --- Classes -------------------------------------------------------------
|
|
229
229
|
|
|
230
230
|
addClass(...names) {
|
|
231
|
-
// Fast path: single class, no spaces
|
|
231
|
+
// Fast path: single class, no spaces - avoids flatMap + regex split allocation
|
|
232
232
|
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
233
233
|
const c = names[0];
|
|
234
234
|
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
|
|
@@ -390,7 +390,7 @@ export class ZQueryCollection {
|
|
|
390
390
|
if (content === undefined) return this.first()?.innerHTML;
|
|
391
391
|
// Auto-morph: if the element already has children, use the diff engine
|
|
392
392
|
// to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
|
|
393
|
-
// Empty elements get raw innerHTML for fast first-paint
|
|
393
|
+
// Empty elements get raw innerHTML for fast first-paint - same strategy
|
|
394
394
|
// the component system uses (first render = innerHTML, updates = morph).
|
|
395
395
|
return this.each((_, el) => {
|
|
396
396
|
if (el.childNodes.length > 0) {
|
|
@@ -575,7 +575,7 @@ export class ZQueryCollection {
|
|
|
575
575
|
if (typeof selectorOrHandler === 'function') {
|
|
576
576
|
el.addEventListener(evt, selectorOrHandler);
|
|
577
577
|
} else if (typeof selectorOrHandler === 'string') {
|
|
578
|
-
// Delegated event
|
|
578
|
+
// Delegated event - store wrapper so off() can remove it
|
|
579
579
|
const wrapper = (e) => {
|
|
580
580
|
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
581
581
|
const target = e.target.closest(selectorOrHandler);
|
|
@@ -637,7 +637,7 @@ export class ZQueryCollection {
|
|
|
637
637
|
// --- Animation -----------------------------------------------------------
|
|
638
638
|
|
|
639
639
|
animate(props, duration = 300, easing = 'ease') {
|
|
640
|
-
// Empty collection
|
|
640
|
+
// Empty collection - resolve immediately
|
|
641
641
|
if (this.length === 0) return Promise.resolve(this);
|
|
642
642
|
return new Promise(resolve => {
|
|
643
643
|
let resolved = false;
|
|
@@ -763,7 +763,7 @@ export class ZQueryCollection {
|
|
|
763
763
|
|
|
764
764
|
|
|
765
765
|
// ---------------------------------------------------------------------------
|
|
766
|
-
// Helper
|
|
766
|
+
// Helper - create document fragment from HTML string
|
|
767
767
|
// ---------------------------------------------------------------------------
|
|
768
768
|
function createFragment(html) {
|
|
769
769
|
const tpl = document.createElement('template');
|
|
@@ -773,21 +773,21 @@ function createFragment(html) {
|
|
|
773
773
|
|
|
774
774
|
|
|
775
775
|
// ---------------------------------------------------------------------------
|
|
776
|
-
// $()
|
|
776
|
+
// $() - main selector / creator (returns ZQueryCollection, like jQuery)
|
|
777
777
|
// ---------------------------------------------------------------------------
|
|
778
778
|
export function query(selector, context) {
|
|
779
779
|
// null / undefined
|
|
780
780
|
if (!selector) return new ZQueryCollection([]);
|
|
781
781
|
|
|
782
|
-
// Already a collection
|
|
782
|
+
// Already a collection - return as-is
|
|
783
783
|
if (selector instanceof ZQueryCollection) return selector;
|
|
784
784
|
|
|
785
|
-
// DOM element or Window
|
|
785
|
+
// DOM element or Window - wrap in collection
|
|
786
786
|
if (selector instanceof Node || selector === window) {
|
|
787
787
|
return new ZQueryCollection([selector]);
|
|
788
788
|
}
|
|
789
789
|
|
|
790
|
-
// NodeList / HTMLCollection / Array
|
|
790
|
+
// NodeList / HTMLCollection / Array - wrap in collection
|
|
791
791
|
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
792
792
|
return new ZQueryCollection(Array.from(selector));
|
|
793
793
|
}
|
|
@@ -811,7 +811,7 @@ export function query(selector, context) {
|
|
|
811
811
|
|
|
812
812
|
|
|
813
813
|
// ---------------------------------------------------------------------------
|
|
814
|
-
// $.all()
|
|
814
|
+
// $.all() - collection selector (returns ZQueryCollection for CSS selectors)
|
|
815
815
|
// ---------------------------------------------------------------------------
|
|
816
816
|
export function queryAll(selector, context) {
|
|
817
817
|
// null / undefined
|
|
@@ -866,7 +866,7 @@ query.children = (parentId) => {
|
|
|
866
866
|
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
867
867
|
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
868
868
|
|
|
869
|
-
// Create element shorthand
|
|
869
|
+
// Create element shorthand - returns ZQueryCollection for chaining
|
|
870
870
|
query.create = (tag, attrs = {}, ...children) => {
|
|
871
871
|
const el = document.createElement(tag);
|
|
872
872
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -889,7 +889,7 @@ query.ready = (fn) => {
|
|
|
889
889
|
else document.addEventListener('DOMContentLoaded', fn);
|
|
890
890
|
};
|
|
891
891
|
|
|
892
|
-
// Global event listeners
|
|
892
|
+
// Global event listeners - supports direct, delegated, and target-bound forms
|
|
893
893
|
// $.on('keydown', handler) → direct listener on document
|
|
894
894
|
// $.on('click', '.btn', handler) → delegated via closest()
|
|
895
895
|
// $.on('scroll', window, handler) → direct listener on target
|
|
@@ -899,7 +899,7 @@ query.on = (event, selectorOrHandler, handler) => {
|
|
|
899
899
|
document.addEventListener(event, selectorOrHandler);
|
|
900
900
|
return;
|
|
901
901
|
}
|
|
902
|
-
// EventTarget (window, element, etc.)
|
|
902
|
+
// EventTarget (window, element, etc.) - direct listener on target
|
|
903
903
|
if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
|
|
904
904
|
selectorOrHandler.addEventListener(event, handler);
|
|
905
905
|
return;
|
package/src/diff.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery Diff
|
|
2
|
+
* zQuery Diff - Lightweight DOM morphing engine
|
|
3
3
|
*
|
|
4
4
|
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
5
5
|
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
* Keyed elements (via `z-key`) get matched across position changes.
|
|
10
10
|
*
|
|
11
11
|
* Performance advantages over virtual DOM (React/Angular):
|
|
12
|
-
* - No virtual tree allocation or diffing
|
|
12
|
+
* - No virtual tree allocation or diffing - works directly on real DOM
|
|
13
13
|
* - Skips unchanged subtrees via fast isEqualNode() check
|
|
14
14
|
* - z-skip attribute to opt out of diffing entire subtrees
|
|
15
15
|
* - Reuses a single template element for HTML parsing (zero GC pressure)
|
|
16
16
|
* - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
|
|
17
|
-
* minimize DOM moves
|
|
17
|
+
* minimize DOM moves - same algorithm as Vue 3 / ivi
|
|
18
18
|
* - Minimal attribute diffing with early bail-out
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
|
-
// Reusable template element
|
|
22
|
+
// Reusable template element - avoids per-call allocation
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
24
|
let _tpl = null;
|
|
25
25
|
|
|
@@ -29,15 +29,15 @@ function _getTemplate() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
|
-
// morph(existingRoot, newHTML)
|
|
32
|
+
// morph(existingRoot, newHTML) - patch existing DOM to match newHTML
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Morph an existing DOM element's children to match new HTML.
|
|
37
37
|
* Only touches nodes that actually differ.
|
|
38
38
|
*
|
|
39
|
-
* @param {Element} rootEl
|
|
40
|
-
* @param {string} newHTML
|
|
39
|
+
* @param {Element} rootEl - The live DOM container to patch
|
|
40
|
+
* @param {string} newHTML - The desired HTML string
|
|
41
41
|
*/
|
|
42
42
|
export function morph(rootEl, newHTML) {
|
|
43
43
|
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
@@ -46,7 +46,7 @@ export function morph(rootEl, newHTML) {
|
|
|
46
46
|
const newRoot = tpl.content;
|
|
47
47
|
|
|
48
48
|
// Move children into a wrapper for consistent handling.
|
|
49
|
-
// We move (not clone) from the template
|
|
49
|
+
// We move (not clone) from the template - cheaper than cloning.
|
|
50
50
|
const tempDiv = document.createElement('div');
|
|
51
51
|
while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
|
|
52
52
|
|
|
@@ -56,16 +56,16 @@ export function morph(rootEl, newHTML) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Morph a single element in place
|
|
59
|
+
* Morph a single element in place - diffs attributes and children
|
|
60
60
|
* without replacing the node reference. Useful for replaceWith-style
|
|
61
61
|
* updates where you want to keep the element identity when the tag
|
|
62
62
|
* name matches.
|
|
63
63
|
*
|
|
64
64
|
* If the new HTML produces a different tag, falls back to native replace.
|
|
65
65
|
*
|
|
66
|
-
* @param {Element} oldEl
|
|
67
|
-
* @param {string} newHTML
|
|
68
|
-
* @returns {Element}
|
|
66
|
+
* @param {Element} oldEl - The live DOM element to patch
|
|
67
|
+
* @param {string} newHTML - HTML string for the replacement element
|
|
68
|
+
* @returns {Element} - The resulting element (same ref if morphed, new if replaced)
|
|
69
69
|
*/
|
|
70
70
|
export function morphElement(oldEl, newHTML) {
|
|
71
71
|
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
@@ -74,7 +74,7 @@ export function morphElement(oldEl, newHTML) {
|
|
|
74
74
|
const newEl = tpl.content.firstElementChild;
|
|
75
75
|
if (!newEl) return oldEl;
|
|
76
76
|
|
|
77
|
-
// Same tag
|
|
77
|
+
// Same tag - morph in place (preserves identity, event listeners, refs)
|
|
78
78
|
if (oldEl.nodeName === newEl.nodeName) {
|
|
79
79
|
_morphAttributes(oldEl, newEl);
|
|
80
80
|
_morphChildren(oldEl, newEl);
|
|
@@ -82,14 +82,14 @@ export function morphElement(oldEl, newHTML) {
|
|
|
82
82
|
return oldEl;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Different tag
|
|
85
|
+
// Different tag - must replace (can't morph <div> into <span>)
|
|
86
86
|
const clone = newEl.cloneNode(true);
|
|
87
87
|
oldEl.parentNode.replaceChild(clone, oldEl);
|
|
88
88
|
if (start) window.__zqMorphHook(clone, performance.now() - start);
|
|
89
89
|
return clone;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// Aliases for the concat build
|
|
92
|
+
// Aliases for the concat build - core.js imports these as _morph / _morphElement,
|
|
93
93
|
// but the build strips `import … as` lines, so the aliases must exist at runtime.
|
|
94
94
|
const _morph = morph;
|
|
95
95
|
const _morphElement = morphElement;
|
|
@@ -97,11 +97,11 @@ const _morphElement = morphElement;
|
|
|
97
97
|
/**
|
|
98
98
|
* Reconcile children of `oldParent` to match `newParent`.
|
|
99
99
|
*
|
|
100
|
-
* @param {Element} oldParent
|
|
101
|
-
* @param {Element} newParent
|
|
100
|
+
* @param {Element} oldParent - live DOM parent
|
|
101
|
+
* @param {Element} newParent - desired state parent
|
|
102
102
|
*/
|
|
103
103
|
function _morphChildren(oldParent, newParent) {
|
|
104
|
-
// Snapshot live NodeLists into arrays
|
|
104
|
+
// Snapshot live NodeLists into arrays - childNodes is live and
|
|
105
105
|
// mutates during insertBefore/removeChild. Using a for loop to push
|
|
106
106
|
// avoids spread operator overhead for large child lists.
|
|
107
107
|
const oldCN = oldParent.childNodes;
|
|
@@ -113,7 +113,7 @@ function _morphChildren(oldParent, newParent) {
|
|
|
113
113
|
for (let i = 0; i < oldLen; i++) oldChildren[i] = oldCN[i];
|
|
114
114
|
for (let i = 0; i < newLen; i++) newChildren[i] = newCN[i];
|
|
115
115
|
|
|
116
|
-
// Scan for keyed elements
|
|
116
|
+
// Scan for keyed elements - only build maps if keys exist
|
|
117
117
|
let hasKeys = false;
|
|
118
118
|
let oldKeyMap, newKeyMap;
|
|
119
119
|
|
|
@@ -144,7 +144,7 @@ function _morphChildren(oldParent, newParent) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
* Unkeyed reconciliation
|
|
147
|
+
* Unkeyed reconciliation - positional matching.
|
|
148
148
|
*/
|
|
149
149
|
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
150
150
|
const oldLen = oldChildren.length;
|
|
@@ -172,7 +172,7 @@ function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/**
|
|
175
|
-
* Keyed reconciliation
|
|
175
|
+
* Keyed reconciliation - match by z-key, reorder with minimal moves
|
|
176
176
|
* using Longest Increasing Subsequence (LIS) to find the maximum set
|
|
177
177
|
* of nodes that are already in the correct relative order, then only
|
|
178
178
|
* move the remaining nodes.
|
|
@@ -206,7 +206,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
206
206
|
|
|
207
207
|
// Step 3: Build index array for LIS of matched old indices.
|
|
208
208
|
// This finds the largest set of keyed nodes already in order,
|
|
209
|
-
// so we only need to move the rest
|
|
209
|
+
// so we only need to move the rest - O(n log n) instead of O(n²).
|
|
210
210
|
const oldIndices = []; // Maps new-position → old-position (or -1)
|
|
211
211
|
for (let i = 0; i < newLen; i++) {
|
|
212
212
|
if (matched[i]) {
|
|
@@ -219,7 +219,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
219
219
|
|
|
220
220
|
const lisSet = _lis(oldIndices);
|
|
221
221
|
|
|
222
|
-
// Step 4: Insert / reorder / morph
|
|
222
|
+
// Step 4: Insert / reorder / morph - walk new children forward,
|
|
223
223
|
// using LIS to decide which nodes stay in place.
|
|
224
224
|
let cursor = oldParent.firstChild;
|
|
225
225
|
const unkeyedOld = [];
|
|
@@ -244,7 +244,7 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
244
244
|
if (!lisSet.has(i)) {
|
|
245
245
|
oldParent.insertBefore(oldNode, cursor);
|
|
246
246
|
}
|
|
247
|
-
// Capture next sibling BEFORE _morphNode
|
|
247
|
+
// Capture next sibling BEFORE _morphNode - if _morphNode calls
|
|
248
248
|
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
249
249
|
const nextSib = oldNode.nextSibling;
|
|
250
250
|
_morphNode(oldParent, oldNode, newNode);
|
|
@@ -284,10 +284,10 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
|
|
|
284
284
|
* Returns a Set of positions (in the input) that form the LIS.
|
|
285
285
|
* Entries with value -1 (unmatched) are excluded.
|
|
286
286
|
*
|
|
287
|
-
* O(n log n)
|
|
287
|
+
* O(n log n) - same algorithm used by Vue 3 and ivi.
|
|
288
288
|
*
|
|
289
|
-
* @param {number[]} arr
|
|
290
|
-
* @returns {Set<number>}
|
|
289
|
+
* @param {number[]} arr - array of old-tree indices (-1 = unmatched)
|
|
290
|
+
* @returns {Set<number>} - positions in arr belonging to the LIS
|
|
291
291
|
*/
|
|
292
292
|
function _lis(arr) {
|
|
293
293
|
const len = arr.length;
|
|
@@ -331,7 +331,7 @@ function _lis(arr) {
|
|
|
331
331
|
* Morph a single node in place.
|
|
332
332
|
*/
|
|
333
333
|
function _morphNode(parent, oldNode, newNode) {
|
|
334
|
-
// Text / comment nodes
|
|
334
|
+
// Text / comment nodes - just update content
|
|
335
335
|
if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
|
|
336
336
|
if (newNode.nodeType === oldNode.nodeType) {
|
|
337
337
|
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
@@ -339,26 +339,26 @@ function _morphNode(parent, oldNode, newNode) {
|
|
|
339
339
|
}
|
|
340
340
|
return;
|
|
341
341
|
}
|
|
342
|
-
// Different node types
|
|
342
|
+
// Different node types - replace
|
|
343
343
|
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
344
344
|
return;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
// Different node types or tag names
|
|
347
|
+
// Different node types or tag names - replace entirely
|
|
348
348
|
if (oldNode.nodeType !== newNode.nodeType ||
|
|
349
349
|
oldNode.nodeName !== newNode.nodeName) {
|
|
350
350
|
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
351
351
|
return;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
// Both are elements
|
|
354
|
+
// Both are elements - diff attributes then recurse children
|
|
355
355
|
if (oldNode.nodeType === 1) {
|
|
356
|
-
// z-skip: developer opt-out
|
|
356
|
+
// z-skip: developer opt-out - skip diffing this subtree entirely.
|
|
357
357
|
// Useful for third-party widgets, canvas, video, or large static content.
|
|
358
358
|
if (oldNode.hasAttribute('z-skip')) return;
|
|
359
359
|
|
|
360
360
|
// Fast bail-out: if the elements are identical, skip everything.
|
|
361
|
-
// isEqualNode() is a native C++ comparison
|
|
361
|
+
// isEqualNode() is a native C++ comparison - much faster than walking
|
|
362
362
|
// attributes + children in JS when trees haven't changed.
|
|
363
363
|
if (oldNode.isEqualNode(newNode)) return;
|
|
364
364
|
|
|
@@ -384,7 +384,7 @@ function _morphNode(parent, oldNode, newNode) {
|
|
|
384
384
|
return;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
-
// Generic element
|
|
387
|
+
// Generic element - recurse children
|
|
388
388
|
_morphChildren(oldNode, newNode);
|
|
389
389
|
}
|
|
390
390
|
}
|
|
@@ -426,7 +426,7 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
// Remove stale attributes
|
|
429
|
+
// Remove stale attributes - snapshot names first because oldAttrs
|
|
430
430
|
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
431
431
|
const oldNames = new Array(oldLen);
|
|
432
432
|
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
@@ -442,7 +442,7 @@ function _morphAttributes(oldEl, newEl) {
|
|
|
442
442
|
*
|
|
443
443
|
* Only updates the value when the new HTML explicitly carries a `value`
|
|
444
444
|
* attribute. Templates that use z-model manage values through reactive
|
|
445
|
-
* state + _bindModels
|
|
445
|
+
* state + _bindModels - the morph engine should not interfere by wiping
|
|
446
446
|
* a live input's content to '' just because the template has no `value`
|
|
447
447
|
* attr. This prevents the wipe-then-restore cycle that resets cursor
|
|
448
448
|
* position on every keystroke.
|
|
@@ -472,14 +472,14 @@ function _syncInputValue(oldEl, newEl) {
|
|
|
472
472
|
*
|
|
473
473
|
* This means the LIS-optimised keyed path activates automatically
|
|
474
474
|
* whenever elements carry `id` or `data-id` / `data-key` attributes
|
|
475
|
-
*
|
|
475
|
+
* - no extra markup required.
|
|
476
476
|
*
|
|
477
477
|
* @returns {string|null}
|
|
478
478
|
*/
|
|
479
479
|
function _getKey(node) {
|
|
480
480
|
if (node.nodeType !== 1) return null;
|
|
481
481
|
|
|
482
|
-
// Explicit z-key
|
|
482
|
+
// Explicit z-key - highest priority
|
|
483
483
|
const zk = node.getAttribute('z-key');
|
|
484
484
|
if (zk) return zk;
|
|
485
485
|
|
package/src/errors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery Errors
|
|
2
|
+
* zQuery Errors - Structured error handling system
|
|
3
3
|
*
|
|
4
4
|
* Provides typed error classes and a configurable error handler so that
|
|
5
5
|
* errors surface consistently across all modules (reactive, component,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
// Error codes
|
|
14
|
+
// Error codes - every zQuery error has a unique code for programmatic use
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
export const ErrorCode = Object.freeze({
|
|
17
17
|
// Reactive
|
|
@@ -61,14 +61,14 @@ export const ErrorCode = Object.freeze({
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
|
-
// ZQueryError
|
|
64
|
+
// ZQueryError - custom error class
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
export class ZQueryError extends Error {
|
|
67
67
|
/**
|
|
68
|
-
* @param {string} code
|
|
69
|
-
* @param {string} message
|
|
70
|
-
* @param {object} [context]
|
|
71
|
-
* @param {Error} [cause]
|
|
68
|
+
* @param {string} code - one of ErrorCode values
|
|
69
|
+
* @param {string} message - human-readable description
|
|
70
|
+
* @param {object} [context] - extra data (component name, expression, etc.)
|
|
71
|
+
* @param {Error} [cause] - original error
|
|
72
72
|
*/
|
|
73
73
|
constructor(code, message, context = {}, cause) {
|
|
74
74
|
super(message);
|
|
@@ -88,10 +88,10 @@ let _errorHandlers = [];
|
|
|
88
88
|
/**
|
|
89
89
|
* Register a global error handler.
|
|
90
90
|
* Called whenever zQuery catches an error internally.
|
|
91
|
-
* Multiple handlers are supported
|
|
91
|
+
* Multiple handlers are supported - each receives the error.
|
|
92
92
|
* Pass `null` to clear all handlers.
|
|
93
93
|
*
|
|
94
|
-
* @param {Function|null} handler
|
|
94
|
+
* @param {Function|null} handler - (error: ZQueryError) => void
|
|
95
95
|
* @returns {Function} unsubscribe function to remove this handler
|
|
96
96
|
*/
|
|
97
97
|
export function onError(handler) {
|
|
@@ -109,9 +109,9 @@ export function onError(handler) {
|
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Report an error through the global handler and console.
|
|
112
|
-
* Non-throwing
|
|
112
|
+
* Non-throwing - used for recoverable errors in callbacks, lifecycle hooks, etc.
|
|
113
113
|
*
|
|
114
|
-
* @param {string} code
|
|
114
|
+
* @param {string} code - ErrorCode
|
|
115
115
|
* @param {string} message
|
|
116
116
|
* @param {object} [context]
|
|
117
117
|
* @param {Error} [cause]
|
|
@@ -135,7 +135,7 @@ export function reportError(code, message, context = {}, cause) {
|
|
|
135
135
|
* the current execution context.
|
|
136
136
|
*
|
|
137
137
|
* @param {Function} fn
|
|
138
|
-
* @param {string} code
|
|
138
|
+
* @param {string} code - ErrorCode to use if the callback throws
|
|
139
139
|
* @param {object} [context]
|
|
140
140
|
* @returns {Function}
|
|
141
141
|
*/
|
|
@@ -154,8 +154,8 @@ export function guardCallback(fn, code, context = {}) {
|
|
|
154
154
|
* Throws ZQueryError on failure (for fast-fail at API boundaries).
|
|
155
155
|
*
|
|
156
156
|
* @param {*} value
|
|
157
|
-
* @param {string} name
|
|
158
|
-
* @param {string} expectedType
|
|
157
|
+
* @param {string} name - parameter name for error message
|
|
158
|
+
* @param {string} expectedType - 'string', 'function', 'object', etc.
|
|
159
159
|
*/
|
|
160
160
|
export function validate(value, name, expectedType) {
|
|
161
161
|
if (value === undefined || value === null) {
|
|
@@ -190,11 +190,11 @@ export function formatError(err) {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
/**
|
|
193
|
-
* Async version of guardCallback
|
|
193
|
+
* Async version of guardCallback - wraps an async function so that
|
|
194
194
|
* rejections are caught, reported, and don't crash execution.
|
|
195
195
|
*
|
|
196
|
-
* @param {Function} fn
|
|
197
|
-
* @param {string} code
|
|
196
|
+
* @param {Function} fn - async function
|
|
197
|
+
* @param {string} code - ErrorCode to use
|
|
198
198
|
* @param {object} [context]
|
|
199
199
|
* @returns {Function}
|
|
200
200
|
*/
|
package/src/expression.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery Expression Parser
|
|
2
|
+
* zQuery Expression Parser - CSP-safe expression evaluator
|
|
3
3
|
*
|
|
4
4
|
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
5
5
|
* evaluates expressions safely without violating Content Security Policy.
|
|
@@ -179,7 +179,7 @@ function tokenize(expr) {
|
|
|
179
179
|
i++; continue;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// Unknown
|
|
182
|
+
// Unknown - skip
|
|
183
183
|
i++;
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -188,7 +188,7 @@ function tokenize(expr) {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
// ---------------------------------------------------------------------------
|
|
191
|
-
// Parser
|
|
191
|
+
// Parser - Pratt (precedence climbing)
|
|
192
192
|
// ---------------------------------------------------------------------------
|
|
193
193
|
class Parser {
|
|
194
194
|
constructor(tokens, scope) {
|
|
@@ -420,7 +420,7 @@ class Parser {
|
|
|
420
420
|
let couldBeArrow = true;
|
|
421
421
|
|
|
422
422
|
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
423
|
-
// () => ...
|
|
423
|
+
// () => ... - no params
|
|
424
424
|
} else {
|
|
425
425
|
while (couldBeArrow) {
|
|
426
426
|
const p = this.peek();
|
|
@@ -446,7 +446,7 @@ class Parser {
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// Not an arrow
|
|
449
|
+
// Not an arrow - restore and parse as grouping
|
|
450
450
|
this.pos = savedPos;
|
|
451
451
|
this.next(); // consume (
|
|
452
452
|
const expr = this.parseExpression(0);
|
|
@@ -539,14 +539,14 @@ class Parser {
|
|
|
539
539
|
return { type: 'ident', name: tok.v };
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
// Fallback
|
|
542
|
+
// Fallback - return undefined for unparseable
|
|
543
543
|
this.next();
|
|
544
544
|
return { type: 'literal', value: undefined };
|
|
545
545
|
}
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
// ---------------------------------------------------------------------------
|
|
549
|
-
// Evaluator
|
|
549
|
+
// Evaluator - walks the AST, resolves against scope
|
|
550
550
|
// ---------------------------------------------------------------------------
|
|
551
551
|
|
|
552
552
|
/** Safe property access whitelist for built-in prototypes */
|
|
@@ -635,8 +635,6 @@ function evaluate(node, scope) {
|
|
|
635
635
|
if (name === 'console') return console;
|
|
636
636
|
if (name === 'Map') return Map;
|
|
637
637
|
if (name === 'Set') return Set;
|
|
638
|
-
if (name === 'RegExp') return RegExp;
|
|
639
|
-
if (name === 'Error') return Error;
|
|
640
638
|
if (name === 'URL') return URL;
|
|
641
639
|
if (name === 'URLSearchParams') return URLSearchParams;
|
|
642
640
|
return undefined;
|
|
@@ -679,7 +677,7 @@ function evaluate(node, scope) {
|
|
|
679
677
|
case 'optional_call': {
|
|
680
678
|
const calleeNode = node.callee;
|
|
681
679
|
const args = _evalArgs(node.args, scope);
|
|
682
|
-
// Method call: obj?.method()
|
|
680
|
+
// Method call: obj?.method() - bind `this` to obj
|
|
683
681
|
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
684
682
|
const obj = evaluate(calleeNode.obj, scope);
|
|
685
683
|
if (obj == null) return undefined;
|
|
@@ -698,9 +696,9 @@ function evaluate(node, scope) {
|
|
|
698
696
|
case 'new': {
|
|
699
697
|
const Ctor = evaluate(node.callee, scope);
|
|
700
698
|
if (typeof Ctor !== 'function') return undefined;
|
|
701
|
-
// Only allow safe constructors
|
|
699
|
+
// Only allow safe constructors (no RegExp - ReDoS risk, no Error - info leak)
|
|
702
700
|
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
703
|
-
Ctor ===
|
|
701
|
+
Ctor === URL || Ctor === URLSearchParams) {
|
|
704
702
|
const args = _evalArgs(node.args, scope);
|
|
705
703
|
return new Ctor(...args);
|
|
706
704
|
}
|
|
@@ -802,7 +800,7 @@ function _resolveCall(node, scope) {
|
|
|
802
800
|
const callee = node.callee;
|
|
803
801
|
const args = _evalArgs(node.args, scope);
|
|
804
802
|
|
|
805
|
-
// Method call: obj.method()
|
|
803
|
+
// Method call: obj.method() - bind `this` to obj
|
|
806
804
|
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
807
805
|
const obj = evaluate(callee.obj, scope);
|
|
808
806
|
if (obj == null) return undefined;
|
|
@@ -868,13 +866,13 @@ function _evalBinary(node, scope) {
|
|
|
868
866
|
/**
|
|
869
867
|
* Safely evaluate a JS expression string against scope layers.
|
|
870
868
|
*
|
|
871
|
-
* @param {string} expr
|
|
872
|
-
* @param {object[]} scope
|
|
869
|
+
* @param {string} expr - expression string
|
|
870
|
+
* @param {object[]} scope - array of scope objects, checked in order
|
|
873
871
|
* Typical: [loopVars, state, { props, refs, $ }]
|
|
874
|
-
* @returns {*}
|
|
872
|
+
* @returns {*} - evaluation result, or undefined on error
|
|
875
873
|
*/
|
|
876
874
|
|
|
877
|
-
// AST cache (LRU)
|
|
875
|
+
// AST cache (LRU) - avoids re-tokenizing and re-parsing the same expression.
|
|
878
876
|
// Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
|
|
879
877
|
// Eviction removes the least-recently-used (first) entry when at capacity.
|
|
880
878
|
const _astCache = new Map();
|
package/src/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery HTTP
|
|
2
|
+
* zQuery HTTP - Lightweight fetch wrapper
|
|
3
3
|
*
|
|
4
4
|
* Clean API for GET/POST/PUT/PATCH/DELETE with:
|
|
5
5
|
* - Auto JSON serialization/deserialization
|
|
@@ -189,7 +189,7 @@ export const http = {
|
|
|
189
189
|
|
|
190
190
|
/**
|
|
191
191
|
* Add request interceptor
|
|
192
|
-
* @param {Function} fn
|
|
192
|
+
* @param {Function} fn - (fetchOpts, url) → void | false | { url, options }
|
|
193
193
|
* @returns {Function} unsubscribe function
|
|
194
194
|
*/
|
|
195
195
|
onRequest(fn) {
|
|
@@ -202,7 +202,7 @@ export const http = {
|
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
204
|
* Add response interceptor
|
|
205
|
-
* @param {Function} fn
|
|
205
|
+
* @param {Function} fn - (result) → void
|
|
206
206
|
* @returns {Function} unsubscribe function
|
|
207
207
|
*/
|
|
208
208
|
onResponse(fn) {
|
|
@@ -214,7 +214,7 @@ export const http = {
|
|
|
214
214
|
},
|
|
215
215
|
|
|
216
216
|
/**
|
|
217
|
-
* Clear interceptors
|
|
217
|
+
* Clear interceptors - all, or just 'request' / 'response'
|
|
218
218
|
*/
|
|
219
219
|
clearInterceptors(type) {
|
|
220
220
|
if (!type || type === 'request') _interceptors.request.length = 0;
|