zero-query 0.9.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +55 -31
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +41 -7
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +4 -2
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +29 -0
  50. package/cli/scaffold/ssr/app/components/about.js +28 -0
  51. package/cli/scaffold/ssr/app/components/home.js +37 -0
  52. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  53. package/cli/scaffold/ssr/app/routes.js +6 -0
  54. package/cli/scaffold/ssr/global.css +113 -0
  55. package/cli/scaffold/ssr/index.html +31 -0
  56. package/cli/scaffold/ssr/package.json +8 -0
  57. package/cli/scaffold/ssr/server/index.js +118 -0
  58. package/cli/utils.js +6 -6
  59. package/dist/zquery.dist.zip +0 -0
  60. package/dist/zquery.js +565 -228
  61. package/dist/zquery.min.js +2 -2
  62. package/index.d.ts +25 -12
  63. package/index.js +11 -7
  64. package/package.json +9 -3
  65. package/src/component.js +64 -63
  66. package/src/core.js +15 -15
  67. package/src/diff.js +38 -38
  68. package/src/errors.js +72 -18
  69. package/src/expression.js +15 -17
  70. package/src/http.js +4 -4
  71. package/src/package.json +1 -0
  72. package/src/reactive.js +75 -9
  73. package/src/router.js +104 -24
  74. package/src/ssr.js +133 -39
  75. package/src/store.js +103 -21
  76. package/src/utils.js +64 -12
  77. package/tests/audit.test.js +143 -15
  78. package/tests/cli.test.js +20 -20
  79. package/tests/component.test.js +121 -121
  80. package/tests/core.test.js +56 -56
  81. package/tests/diff.test.js +42 -42
  82. package/tests/errors.test.js +425 -147
  83. package/tests/expression.test.js +58 -53
  84. package/tests/http.test.js +20 -20
  85. package/tests/reactive.test.js +185 -24
  86. package/tests/router.test.js +501 -74
  87. package/tests/ssr.test.js +444 -10
  88. package/tests/store.test.js +264 -23
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +36 -4
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +22 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
package/src/core.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery Core Selector engine & chainable DOM collection
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 wraps an array of elements with chainable methods
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 avoids flatMap + regex split allocation
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 same strategy
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 store wrapper so off() can remove it
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 resolve immediately
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 create document fragment from HTML string
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
- // $() main selector / creator (returns ZQueryCollection, like jQuery)
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 return as-is
782
+ // Already a collection - return as-is
783
783
  if (selector instanceof ZQueryCollection) return selector;
784
784
 
785
- // DOM element or Window wrap in collection
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 wrap in collection
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() collection selector (returns ZQueryCollection for CSS selectors)
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 returns ZQueryCollection for chaining
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 supports direct, delegated, and target-bound forms
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.) direct listener on target
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 Lightweight DOM morphing engine
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 works directly on real DOM
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 same algorithm as Vue 3 / ivi
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 avoids per-call allocation
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) patch existing DOM to match 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 The live DOM container to patch
40
- * @param {string} newHTML The desired HTML string
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 cheaper than cloning.
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 diffs attributes and children
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 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)
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 morph in place (preserves identity, event listeners, refs)
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 must replace (can't morph <div> into <span>)
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 core.js imports these as _morph / _morphElement,
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 live DOM parent
101
- * @param {Element} newParent desired state parent
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 childNodes is live and
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 only build maps if keys exist
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 positional matching.
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 match by z-key, reorder with minimal moves
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 O(n log n) instead of O(n²).
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 walk new children forward,
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 if _morphNode calls
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) same algorithm used by Vue 3 and ivi.
287
+ * O(n log n) - same algorithm used by Vue 3 and ivi.
288
288
  *
289
- * @param {number[]} arr array of old-tree indices (-1 = unmatched)
290
- * @returns {Set<number>} positions in arr belonging to the LIS
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 just update content
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 replace
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 replace entirely
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 diff attributes then recurse children
354
+ // Both are elements - diff attributes then recurse children
355
355
  if (oldNode.nodeType === 1) {
356
- // z-skip: developer opt-out skip diffing this subtree entirely.
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 much faster than walking
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 recurse children
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 snapshot names first because oldAttrs
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 the morph engine should not interfere by wiping
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
- * no extra markup required.
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 highest priority
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 Structured error handling system
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 every zQuery error has a unique code for programmatic use
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
@@ -49,20 +49,26 @@ export const ErrorCode = Object.freeze({
49
49
  HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
50
50
  HTTP_PARSE: 'ZQ_HTTP_PARSE',
51
51
 
52
+ // SSR
53
+ SSR_RENDER: 'ZQ_SSR_RENDER',
54
+ SSR_COMPONENT: 'ZQ_SSR_COMPONENT',
55
+ SSR_HYDRATION: 'ZQ_SSR_HYDRATION',
56
+ SSR_PAGE: 'ZQ_SSR_PAGE',
57
+
52
58
  // General
53
59
  INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
54
60
  });
55
61
 
56
62
 
57
63
  // ---------------------------------------------------------------------------
58
- // ZQueryError custom error class
64
+ // ZQueryError - custom error class
59
65
  // ---------------------------------------------------------------------------
60
66
  export class ZQueryError extends Error {
61
67
  /**
62
- * @param {string} code one of ErrorCode values
63
- * @param {string} message human-readable description
64
- * @param {object} [context] extra data (component name, expression, etc.)
65
- * @param {Error} [cause] original error
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
66
72
  */
67
73
  constructor(code, message, context = {}, cause) {
68
74
  super(message);
@@ -77,23 +83,35 @@ export class ZQueryError extends Error {
77
83
  // ---------------------------------------------------------------------------
78
84
  // Global error handler
79
85
  // ---------------------------------------------------------------------------
80
- let _errorHandler = null;
86
+ let _errorHandlers = [];
81
87
 
82
88
  /**
83
89
  * Register a global error handler.
84
90
  * Called whenever zQuery catches an error internally.
91
+ * Multiple handlers are supported - each receives the error.
92
+ * Pass `null` to clear all handlers.
85
93
  *
86
- * @param {Function|null} handler (error: ZQueryError) => void
94
+ * @param {Function|null} handler - (error: ZQueryError) => void
95
+ * @returns {Function} unsubscribe function to remove this handler
87
96
  */
88
97
  export function onError(handler) {
89
- _errorHandler = typeof handler === 'function' ? handler : null;
98
+ if (handler === null) {
99
+ _errorHandlers = [];
100
+ return () => {};
101
+ }
102
+ if (typeof handler !== 'function') return () => {};
103
+ _errorHandlers.push(handler);
104
+ return () => {
105
+ const idx = _errorHandlers.indexOf(handler);
106
+ if (idx !== -1) _errorHandlers.splice(idx, 1);
107
+ };
90
108
  }
91
109
 
92
110
  /**
93
111
  * Report an error through the global handler and console.
94
- * Non-throwing used for recoverable errors in callbacks, lifecycle hooks, etc.
112
+ * Non-throwing - used for recoverable errors in callbacks, lifecycle hooks, etc.
95
113
  *
96
- * @param {string} code ErrorCode
114
+ * @param {string} code - ErrorCode
97
115
  * @param {string} message
98
116
  * @param {object} [context]
99
117
  * @param {Error} [cause]
@@ -103,9 +121,9 @@ export function reportError(code, message, context = {}, cause) {
103
121
  ? cause
104
122
  : new ZQueryError(code, message, context, cause);
105
123
 
106
- // User handler gets first crack
107
- if (_errorHandler) {
108
- try { _errorHandler(err); } catch { /* prevent handler from crashing framework */ }
124
+ // Notify all registered handlers
125
+ for (const handler of _errorHandlers) {
126
+ try { handler(err); } catch { /* prevent handler from crashing framework */ }
109
127
  }
110
128
 
111
129
  // Always log for developer visibility
@@ -117,7 +135,7 @@ export function reportError(code, message, context = {}, cause) {
117
135
  * the current execution context.
118
136
  *
119
137
  * @param {Function} fn
120
- * @param {string} code ErrorCode to use if the callback throws
138
+ * @param {string} code - ErrorCode to use if the callback throws
121
139
  * @param {object} [context]
122
140
  * @returns {Function}
123
141
  */
@@ -136,8 +154,8 @@ export function guardCallback(fn, code, context = {}) {
136
154
  * Throws ZQueryError on failure (for fast-fail at API boundaries).
137
155
  *
138
156
  * @param {*} value
139
- * @param {string} name parameter name for error message
140
- * @param {string} expectedType 'string', 'function', 'object', etc.
157
+ * @param {string} name - parameter name for error message
158
+ * @param {string} expectedType - 'string', 'function', 'object', etc.
141
159
  */
142
160
  export function validate(value, name, expectedType) {
143
161
  if (value === undefined || value === null) {
@@ -153,3 +171,39 @@ export function validate(value, name, expectedType) {
153
171
  );
154
172
  }
155
173
  }
174
+
175
+ /**
176
+ * Format a ZQueryError into a structured object suitable for overlays/logging.
177
+ * @param {ZQueryError|Error} err
178
+ * @returns {{ code: string, type: string, message: string, context: object, stack: string }}
179
+ */
180
+ export function formatError(err) {
181
+ const isZQ = err instanceof ZQueryError;
182
+ return {
183
+ code: isZQ ? err.code : '',
184
+ type: isZQ ? 'ZQueryError' : (err.name || 'Error'),
185
+ message: err.message || 'Unknown error',
186
+ context: isZQ ? err.context : {},
187
+ stack: err.stack || '',
188
+ cause: err.cause ? formatError(err.cause) : null,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Async version of guardCallback - wraps an async function so that
194
+ * rejections are caught, reported, and don't crash execution.
195
+ *
196
+ * @param {Function} fn - async function
197
+ * @param {string} code - ErrorCode to use
198
+ * @param {object} [context]
199
+ * @returns {Function}
200
+ */
201
+ export function guardAsync(fn, code, context = {}) {
202
+ return async (...args) => {
203
+ try {
204
+ return await fn(...args);
205
+ } catch (err) {
206
+ reportError(code, err.message || 'Async callback error', context, err);
207
+ }
208
+ };
209
+ }
package/src/expression.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery Expression Parser CSP-safe expression evaluator
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 skip
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 Pratt (precedence climbing)
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
- // () => ... no params
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 restore and parse as grouping
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 return undefined for unparseable
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 walks the AST, resolves against scope
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() bind `this` to obj
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 === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
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() bind `this` to obj
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 expression string
872
- * @param {object[]} scope array of scope objects, checked in order
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 {*} evaluation result, or undefined on error
872
+ * @returns {*} - evaluation result, or undefined on error
875
873
  */
876
874
 
877
- // AST cache (LRU) avoids re-tokenizing and re-parsing the same expression.
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();