zero-query 0.4.9 → 0.6.3
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 +16 -10
- package/cli/commands/build.js +4 -2
- package/cli/commands/bundle.js +113 -10
- package/cli/commands/dev/index.js +82 -0
- package/cli/commands/dev/logger.js +70 -0
- package/cli/commands/dev/overlay.js +317 -0
- package/cli/commands/dev/server.js +129 -0
- package/cli/commands/dev/validator.js +94 -0
- package/cli/commands/dev/watcher.js +114 -0
- package/cli/commands/{dev.js → dev.old.js} +8 -4
- package/cli/help.js +18 -6
- package/cli/scaffold/favicon.ico +0 -0
- package/cli/scaffold/scripts/components/about.js +14 -2
- package/cli/scaffold/scripts/components/contacts/contacts.html +5 -4
- 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/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1550 -97
- package/dist/zquery.min.js +11 -8
- package/index.d.ts +253 -14
- package/index.js +25 -8
- package/package.json +8 -2
- package/src/component.js +175 -44
- package/src/core.js +25 -18
- 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 +11 -5
- package/src/ssr.js +224 -0
- package/src/store.js +24 -8
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
|
+
}
|