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.
Files changed (58) hide show
  1. package/README.md +12 -10
  2. package/cli/commands/build.js +7 -5
  3. package/cli/commands/bundle.js +286 -8
  4. package/cli/commands/dev/index.js +82 -0
  5. package/cli/commands/dev/logger.js +70 -0
  6. package/cli/commands/dev/overlay.js +366 -0
  7. package/cli/commands/dev/server.js +158 -0
  8. package/cli/commands/dev/validator.js +94 -0
  9. package/cli/commands/dev/watcher.js +147 -0
  10. package/cli/scaffold/favicon.ico +0 -0
  11. package/cli/scaffold/index.html +1 -0
  12. package/cli/scaffold/scripts/app.js +15 -22
  13. package/cli/scaffold/scripts/components/about.js +14 -2
  14. package/cli/scaffold/scripts/components/contacts/contacts.css +0 -7
  15. package/cli/scaffold/scripts/components/contacts/contacts.html +8 -7
  16. package/cli/scaffold/scripts/components/contacts/contacts.js +17 -1
  17. package/cli/scaffold/scripts/components/counter.js +30 -10
  18. package/cli/scaffold/scripts/components/home.js +3 -3
  19. package/cli/scaffold/scripts/components/todos.js +6 -5
  20. package/cli/scaffold/styles/styles.css +1 -0
  21. package/cli/utils.js +111 -6
  22. package/dist/zquery.dist.zip +0 -0
  23. package/dist/zquery.js +2005 -216
  24. package/dist/zquery.min.js +3 -13
  25. package/index.d.ts +149 -1080
  26. package/index.js +18 -7
  27. package/package.json +9 -3
  28. package/src/component.js +186 -45
  29. package/src/core.js +327 -35
  30. package/src/diff.js +280 -0
  31. package/src/errors.js +155 -0
  32. package/src/expression.js +806 -0
  33. package/src/http.js +18 -10
  34. package/src/reactive.js +29 -4
  35. package/src/router.js +59 -6
  36. package/src/ssr.js +224 -0
  37. package/src/store.js +24 -8
  38. package/tests/component.test.js +304 -0
  39. package/tests/core.test.js +726 -0
  40. package/tests/diff.test.js +194 -0
  41. package/tests/errors.test.js +162 -0
  42. package/tests/expression.test.js +334 -0
  43. package/tests/http.test.js +181 -0
  44. package/tests/reactive.test.js +191 -0
  45. package/tests/router.test.js +332 -0
  46. package/tests/store.test.js +253 -0
  47. package/tests/utils.test.js +353 -0
  48. package/types/collection.d.ts +368 -0
  49. package/types/component.d.ts +210 -0
  50. package/types/errors.d.ts +103 -0
  51. package/types/http.d.ts +81 -0
  52. package/types/misc.d.ts +166 -0
  53. package/types/reactive.d.ts +76 -0
  54. package/types/router.d.ts +132 -0
  55. package/types/ssr.d.ts +49 -0
  56. package/types/store.d.ts +107 -0
  57. package/types/utils.d.ts +142 -0
  58. /package/cli/commands/{dev.js → dev.old.js} +0 -0
package/src/diff.js ADDED
@@ -0,0 +1,280 @@
1
+ /**
2
+ * zQuery Diff — Lightweight DOM morphing engine
3
+ *
4
+ * Patches an existing DOM tree to match new HTML without destroying nodes
5
+ * that haven't changed. Preserves focus, scroll positions, third-party
6
+ * widget state, video playback, and other live DOM state.
7
+ *
8
+ * Approach: walk old and new trees in parallel, reconcile node by node.
9
+ * Keyed elements (via `z-key`) get matched across position changes.
10
+ */
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // morph(existingRoot, newHTML) — patch existing DOM to match newHTML
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Morph an existing DOM element's children to match new HTML.
18
+ * Only touches nodes that actually differ.
19
+ *
20
+ * @param {Element} rootEl — The live DOM container to patch
21
+ * @param {string} newHTML — The desired HTML string
22
+ */
23
+ export function morph(rootEl, newHTML) {
24
+ const template = document.createElement('template');
25
+ template.innerHTML = newHTML;
26
+ const newRoot = template.content;
27
+
28
+ // Convert to element for consistent handling — wrap in a div if needed
29
+ const tempDiv = document.createElement('div');
30
+ while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
31
+
32
+ _morphChildren(rootEl, tempDiv);
33
+ }
34
+
35
+ /**
36
+ * Reconcile children of `oldParent` to match `newParent`.
37
+ *
38
+ * @param {Element} oldParent — live DOM parent
39
+ * @param {Element} newParent — desired state parent
40
+ */
41
+ function _morphChildren(oldParent, newParent) {
42
+ const oldChildren = [...oldParent.childNodes];
43
+ const newChildren = [...newParent.childNodes];
44
+
45
+ // Build key maps for keyed element matching
46
+ const oldKeyMap = new Map();
47
+ const newKeyMap = new Map();
48
+
49
+ for (let i = 0; i < oldChildren.length; i++) {
50
+ const key = _getKey(oldChildren[i]);
51
+ if (key != null) oldKeyMap.set(key, i);
52
+ }
53
+ for (let i = 0; i < newChildren.length; i++) {
54
+ const key = _getKey(newChildren[i]);
55
+ if (key != null) newKeyMap.set(key, i);
56
+ }
57
+
58
+ const hasKeys = oldKeyMap.size > 0 || newKeyMap.size > 0;
59
+
60
+ if (hasKeys) {
61
+ _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap);
62
+ } else {
63
+ _morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Unkeyed reconciliation — positional matching.
69
+ */
70
+ function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
71
+ const maxLen = Math.max(oldChildren.length, newChildren.length);
72
+
73
+ for (let i = 0; i < maxLen; i++) {
74
+ const oldNode = oldChildren[i];
75
+ const newNode = newChildren[i];
76
+
77
+ if (!oldNode && newNode) {
78
+ // New node — append
79
+ oldParent.appendChild(newNode.cloneNode(true));
80
+ } else if (oldNode && !newNode) {
81
+ // Extra old node — remove
82
+ oldParent.removeChild(oldNode);
83
+ } else if (oldNode && newNode) {
84
+ _morphNode(oldParent, oldNode, newNode);
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Keyed reconciliation — match by z-key, reorder minimal moves.
91
+ */
92
+ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap) {
93
+ // Track which old nodes are consumed
94
+ const consumed = new Set();
95
+
96
+ // Step 1: Build ordered list of matched old nodes for new children
97
+ const newLen = newChildren.length;
98
+ const matched = new Array(newLen); // matched[newIdx] = oldNode | null
99
+
100
+ for (let i = 0; i < newLen; i++) {
101
+ const key = _getKey(newChildren[i]);
102
+ if (key != null && oldKeyMap.has(key)) {
103
+ const oldIdx = oldKeyMap.get(key);
104
+ matched[i] = oldChildren[oldIdx];
105
+ consumed.add(oldIdx);
106
+ } else {
107
+ matched[i] = null;
108
+ }
109
+ }
110
+
111
+ // Step 2: Remove old nodes that are not in the new tree
112
+ for (let i = oldChildren.length - 1; i >= 0; i--) {
113
+ if (!consumed.has(i)) {
114
+ const key = _getKey(oldChildren[i]);
115
+ if (key != null && !newKeyMap.has(key)) {
116
+ oldParent.removeChild(oldChildren[i]);
117
+ } else if (key == null) {
118
+ // Unkeyed old node — will be handled positionally below
119
+ }
120
+ }
121
+ }
122
+
123
+ // Step 3: Insert/reorder/morph
124
+ let cursor = oldParent.firstChild;
125
+ const unkeyedOld = oldChildren.filter((n, i) => !consumed.has(i) && _getKey(n) == null);
126
+ let unkeyedIdx = 0;
127
+
128
+ for (let i = 0; i < newLen; i++) {
129
+ const newNode = newChildren[i];
130
+ const newKey = _getKey(newNode);
131
+ let oldNode = matched[i];
132
+
133
+ if (!oldNode && newKey == null) {
134
+ // Try to match an unkeyed old node positionally
135
+ oldNode = unkeyedOld[unkeyedIdx++] || null;
136
+ }
137
+
138
+ if (oldNode) {
139
+ // Move into position if needed
140
+ if (oldNode !== cursor) {
141
+ oldParent.insertBefore(oldNode, cursor);
142
+ }
143
+ // Morph in place
144
+ _morphNode(oldParent, oldNode, newNode);
145
+ cursor = oldNode.nextSibling;
146
+ } else {
147
+ // Insert new node
148
+ const clone = newNode.cloneNode(true);
149
+ if (cursor) {
150
+ oldParent.insertBefore(clone, cursor);
151
+ } else {
152
+ oldParent.appendChild(clone);
153
+ }
154
+ // cursor stays the same — new node is before it
155
+ }
156
+ }
157
+
158
+ // Remove any remaining unkeyed old nodes at the end
159
+ while (unkeyedIdx < unkeyedOld.length) {
160
+ const leftover = unkeyedOld[unkeyedIdx++];
161
+ if (leftover.parentNode === oldParent) {
162
+ oldParent.removeChild(leftover);
163
+ }
164
+ }
165
+
166
+ // Remove any remaining keyed old nodes that weren't consumed
167
+ for (let i = 0; i < oldChildren.length; i++) {
168
+ if (!consumed.has(i)) {
169
+ const node = oldChildren[i];
170
+ if (node.parentNode === oldParent && _getKey(node) != null && !newKeyMap.has(_getKey(node))) {
171
+ oldParent.removeChild(node);
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Morph a single node in place.
179
+ */
180
+ function _morphNode(parent, oldNode, newNode) {
181
+ // Text / comment nodes — just update content
182
+ if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
183
+ if (newNode.nodeType === oldNode.nodeType) {
184
+ if (oldNode.nodeValue !== newNode.nodeValue) {
185
+ oldNode.nodeValue = newNode.nodeValue;
186
+ }
187
+ return;
188
+ }
189
+ // Different node types — replace
190
+ parent.replaceChild(newNode.cloneNode(true), oldNode);
191
+ return;
192
+ }
193
+
194
+ // Different node types or tag names — replace entirely
195
+ if (oldNode.nodeType !== newNode.nodeType ||
196
+ oldNode.nodeName !== newNode.nodeName) {
197
+ parent.replaceChild(newNode.cloneNode(true), oldNode);
198
+ return;
199
+ }
200
+
201
+ // Both are elements — diff attributes then recurse children
202
+ if (oldNode.nodeType === 1) {
203
+ _morphAttributes(oldNode, newNode);
204
+
205
+ // Special elements: don't recurse into their children
206
+ // (textarea value, input value, select, etc.)
207
+ const tag = oldNode.nodeName;
208
+ if (tag === 'INPUT') {
209
+ _syncInputValue(oldNode, newNode);
210
+ return;
211
+ }
212
+ if (tag === 'TEXTAREA') {
213
+ if (oldNode.value !== newNode.textContent) {
214
+ oldNode.value = newNode.textContent || '';
215
+ }
216
+ return;
217
+ }
218
+ if (tag === 'SELECT') {
219
+ // Recurse children (options) then sync value
220
+ _morphChildren(oldNode, newNode);
221
+ if (oldNode.value !== newNode.value) {
222
+ oldNode.value = newNode.value;
223
+ }
224
+ return;
225
+ }
226
+
227
+ // Generic element — recurse children
228
+ _morphChildren(oldNode, newNode);
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Sync attributes from newEl onto oldEl.
234
+ */
235
+ function _morphAttributes(oldEl, newEl) {
236
+ // Add/update attributes
237
+ const newAttrs = newEl.attributes;
238
+ for (let i = 0; i < newAttrs.length; i++) {
239
+ const attr = newAttrs[i];
240
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
241
+ oldEl.setAttribute(attr.name, attr.value);
242
+ }
243
+ }
244
+
245
+ // Remove stale attributes
246
+ const oldAttrs = oldEl.attributes;
247
+ for (let i = oldAttrs.length - 1; i >= 0; i--) {
248
+ const attr = oldAttrs[i];
249
+ if (!newEl.hasAttribute(attr.name)) {
250
+ oldEl.removeAttribute(attr.name);
251
+ }
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Sync input element value, checked, disabled states.
257
+ */
258
+ function _syncInputValue(oldEl, newEl) {
259
+ const type = (oldEl.type || '').toLowerCase();
260
+
261
+ if (type === 'checkbox' || type === 'radio') {
262
+ if (oldEl.checked !== newEl.checked) oldEl.checked = newEl.checked;
263
+ } else {
264
+ if (oldEl.value !== (newEl.getAttribute('value') || '')) {
265
+ oldEl.value = newEl.getAttribute('value') || '';
266
+ }
267
+ }
268
+
269
+ // Sync disabled
270
+ if (oldEl.disabled !== newEl.disabled) oldEl.disabled = newEl.disabled;
271
+ }
272
+
273
+ /**
274
+ * Get the reconciliation key from a node (z-key attribute).
275
+ * @returns {string|null}
276
+ */
277
+ function _getKey(node) {
278
+ if (node.nodeType !== 1) return null;
279
+ return node.getAttribute('z-key') || null;
280
+ }
package/src/errors.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * zQuery Errors — Structured error handling system
3
+ *
4
+ * Provides typed error classes and a configurable error handler so that
5
+ * errors surface consistently across all modules (reactive, component,
6
+ * router, store, expression parser, HTTP, etc.).
7
+ *
8
+ * Default behaviour: errors are logged via console.warn/error.
9
+ * Users can override with $.onError(handler) to integrate with their
10
+ * own logging, crash-reporting, or UI notification system.
11
+ */
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Error codes — every zQuery error has a unique code for programmatic use
15
+ // ---------------------------------------------------------------------------
16
+ export const ErrorCode = Object.freeze({
17
+ // Reactive
18
+ REACTIVE_CALLBACK: 'ZQ_REACTIVE_CALLBACK',
19
+ SIGNAL_CALLBACK: 'ZQ_SIGNAL_CALLBACK',
20
+ EFFECT_EXEC: 'ZQ_EFFECT_EXEC',
21
+
22
+ // Expression parser
23
+ EXPR_PARSE: 'ZQ_EXPR_PARSE',
24
+ EXPR_EVAL: 'ZQ_EXPR_EVAL',
25
+ EXPR_UNSAFE_ACCESS: 'ZQ_EXPR_UNSAFE_ACCESS',
26
+
27
+ // Component
28
+ COMP_INVALID_NAME: 'ZQ_COMP_INVALID_NAME',
29
+ COMP_NOT_FOUND: 'ZQ_COMP_NOT_FOUND',
30
+ COMP_MOUNT_TARGET: 'ZQ_COMP_MOUNT_TARGET',
31
+ COMP_RENDER: 'ZQ_COMP_RENDER',
32
+ COMP_LIFECYCLE: 'ZQ_COMP_LIFECYCLE',
33
+ COMP_RESOURCE: 'ZQ_COMP_RESOURCE',
34
+ COMP_DIRECTIVE: 'ZQ_COMP_DIRECTIVE',
35
+
36
+ // Router
37
+ ROUTER_LOAD: 'ZQ_ROUTER_LOAD',
38
+ ROUTER_GUARD: 'ZQ_ROUTER_GUARD',
39
+ ROUTER_RESOLVE: 'ZQ_ROUTER_RESOLVE',
40
+
41
+ // Store
42
+ STORE_ACTION: 'ZQ_STORE_ACTION',
43
+ STORE_MIDDLEWARE: 'ZQ_STORE_MIDDLEWARE',
44
+ STORE_SUBSCRIBE: 'ZQ_STORE_SUBSCRIBE',
45
+
46
+ // HTTP
47
+ HTTP_REQUEST: 'ZQ_HTTP_REQUEST',
48
+ HTTP_TIMEOUT: 'ZQ_HTTP_TIMEOUT',
49
+ HTTP_INTERCEPTOR: 'ZQ_HTTP_INTERCEPTOR',
50
+ HTTP_PARSE: 'ZQ_HTTP_PARSE',
51
+
52
+ // General
53
+ INVALID_ARGUMENT: 'ZQ_INVALID_ARGUMENT',
54
+ });
55
+
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // ZQueryError — custom error class
59
+ // ---------------------------------------------------------------------------
60
+ export class ZQueryError extends Error {
61
+ /**
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
66
+ */
67
+ constructor(code, message, context = {}, cause) {
68
+ super(message);
69
+ this.name = 'ZQueryError';
70
+ this.code = code;
71
+ this.context = context;
72
+ if (cause) this.cause = cause;
73
+ }
74
+ }
75
+
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Global error handler
79
+ // ---------------------------------------------------------------------------
80
+ let _errorHandler = null;
81
+
82
+ /**
83
+ * Register a global error handler.
84
+ * Called whenever zQuery catches an error internally.
85
+ *
86
+ * @param {Function|null} handler — (error: ZQueryError) => void
87
+ */
88
+ export function onError(handler) {
89
+ _errorHandler = typeof handler === 'function' ? handler : null;
90
+ }
91
+
92
+ /**
93
+ * Report an error through the global handler and console.
94
+ * Non-throwing — used for recoverable errors in callbacks, lifecycle hooks, etc.
95
+ *
96
+ * @param {string} code — ErrorCode
97
+ * @param {string} message
98
+ * @param {object} [context]
99
+ * @param {Error} [cause]
100
+ */
101
+ export function reportError(code, message, context = {}, cause) {
102
+ const err = cause instanceof ZQueryError
103
+ ? cause
104
+ : new ZQueryError(code, message, context, cause);
105
+
106
+ // User handler gets first crack
107
+ if (_errorHandler) {
108
+ try { _errorHandler(err); } catch { /* prevent handler from crashing framework */ }
109
+ }
110
+
111
+ // Always log for developer visibility
112
+ console.error(`[zQuery ${code}] ${message}`, context, cause || '');
113
+ }
114
+
115
+ /**
116
+ * Wrap a callback so that thrown errors are caught, reported, and don't crash
117
+ * the current execution context.
118
+ *
119
+ * @param {Function} fn
120
+ * @param {string} code — ErrorCode to use if the callback throws
121
+ * @param {object} [context]
122
+ * @returns {Function}
123
+ */
124
+ export function guardCallback(fn, code, context = {}) {
125
+ return (...args) => {
126
+ try {
127
+ return fn(...args);
128
+ } catch (err) {
129
+ reportError(code, err.message || 'Callback error', context, err);
130
+ }
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Validate a required value is defined and of the expected type.
136
+ * Throws ZQueryError on failure (for fast-fail at API boundaries).
137
+ *
138
+ * @param {*} value
139
+ * @param {string} name — parameter name for error message
140
+ * @param {string} expectedType — 'string', 'function', 'object', etc.
141
+ */
142
+ export function validate(value, name, expectedType) {
143
+ if (value === undefined || value === null) {
144
+ throw new ZQueryError(
145
+ ErrorCode.INVALID_ARGUMENT,
146
+ `"${name}" is required but got ${value}`
147
+ );
148
+ }
149
+ if (expectedType && typeof value !== expectedType) {
150
+ throw new ZQueryError(
151
+ ErrorCode.INVALID_ARGUMENT,
152
+ `"${name}" must be a ${expectedType}, got ${typeof value}`
153
+ );
154
+ }
155
+ }