zero-query 0.9.0 → 0.9.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 CHANGED
@@ -262,14 +262,13 @@ location / {
262
262
  | `$.router` `$.getRouter` | SPA router |
263
263
  | `$.store` `$.getStore` | State management |
264
264
  | `$.http` `$.get` `$.post` `$.put` `$.patch` `$.delete` | HTTP client |
265
- | `$.reactive` `$.signal` `$.computed` `$.effect` | Reactive primitives |
265
+ | `$.reactive` `$.Signal` `$.signal` `$.computed` `$.effect` | Reactive primitives |
266
266
  | `$.debounce` `$.throttle` `$.pipe` `$.once` `$.sleep` | Function utils |
267
267
  | `$.escapeHtml` `$.html` `$.trust` `$.uuid` `$.camelCase` `$.kebabCase` | String utils |
268
268
  | `$.deepClone` `$.deepMerge` `$.isEqual` | Object utils |
269
269
  | `$.param` `$.parseQuery` | URL utils |
270
270
  | `$.storage` `$.session` | Storage wrappers |
271
- | `$.bus` | Event bus |
272
- | `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
271
+ | `$.bus` | Event bus || `$.onError` `$.ZQueryError` `$.ErrorCode` `$.guardCallback` `$.validate` | Error handling || `$.version` | Library version |\n| `$.libSize` | Minified bundle size string (e.g. `\"~91 KB\"`) |
273
272
  | `$.meta` | Build metadata (populated by CLI bundler) |
274
273
  | `$.noConflict` | Release `$` global |
275
274
 
@@ -35,12 +35,25 @@ function resolveImport(specifier, fromFile) {
35
35
 
36
36
  /** Extract import specifiers from a source file. */
37
37
  function extractImports(code) {
38
+ // Only scan the import preamble (before the first top-level `export`)
39
+ // so that code examples inside exported template strings are not
40
+ // mistaken for real imports.
41
+ const exportStart = code.search(/^export\b/m);
42
+ const preamble = exportStart > -1 ? code.slice(0, exportStart) : code;
43
+
38
44
  const specifiers = [];
39
45
  let m;
40
46
  const fromRe = /\bfrom\s+['"]([^'"]+)['"]/g;
41
- while ((m = fromRe.exec(code)) !== null) specifiers.push(m[1]);
47
+ while ((m = fromRe.exec(preamble)) !== null) specifiers.push(m[1]);
42
48
  const sideRe = /^\s*import\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
43
- while ((m = sideRe.exec(code)) !== null) {
49
+ while ((m = sideRe.exec(preamble)) !== null) {
50
+ if (!specifiers.includes(m[1])) specifiers.push(m[1]);
51
+ }
52
+
53
+ // Also capture re-exports anywhere in the file:
54
+ // export { x } from '...' export * from '...'
55
+ const reExportRe = /^\s*export\s+(?:\{[^}]*\}|\*)\s*from\s+['"]([^'"]+)['"]/gm;
56
+ while ((m = reExportRe.exec(code)) !== null) {
44
57
  if (!specifiers.includes(m[1])) specifiers.push(m[1]);
45
58
  }
46
59
  return specifiers;
Binary file
package/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.9.0
2
+ * zQuery (zeroQuery) v0.9.1
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -300,7 +300,13 @@ function signal(initial) {
300
300
  */
301
301
  function computed(fn) {
302
302
  const s = new Signal(undefined);
303
- effect(() => { s._value = fn(); s._notify(); });
303
+ effect(() => {
304
+ const v = fn();
305
+ if (v !== s._value) {
306
+ s._value = v;
307
+ s._notify();
308
+ }
309
+ });
304
310
  return s;
305
311
  }
306
312
 
@@ -343,7 +349,7 @@ function effect(fn) {
343
349
  }
344
350
  execute._deps.clear();
345
351
  }
346
- Signal._activeEffect = null;
352
+ // Don't clobber _activeEffect another effect may be running
347
353
  };
348
354
  }
349
355
 
@@ -361,7 +367,7 @@ function effect(fn) {
361
367
  // ---------------------------------------------------------------------------
362
368
  class ZQueryCollection {
363
369
  constructor(elements) {
364
- this.elements = Array.isArray(elements) ? elements : [elements];
370
+ this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
365
371
  this.length = this.elements.length;
366
372
  this.elements.forEach((el, i) => { this[i] = el; });
367
373
  }
@@ -418,10 +424,12 @@ class ZQueryCollection {
418
424
  return new ZQueryCollection([...kids]);
419
425
  }
420
426
 
421
- siblings() {
427
+ siblings(selector) {
422
428
  const sibs = [];
423
429
  this.elements.forEach(el => {
424
- sibs.push(...[...el.parentElement.children].filter(c => c !== el));
430
+ if (!el.parentElement) return;
431
+ const all = [...el.parentElement.children].filter(c => c !== el);
432
+ sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
425
433
  });
426
434
  return new ZQueryCollection(sibs);
427
435
  }
@@ -563,7 +571,8 @@ class ZQueryCollection {
563
571
  index(selector) {
564
572
  if (selector === undefined) {
565
573
  const el = this.first();
566
- return el ? Array.from(el.parentElement.children).indexOf(el) : -1;
574
+ if (!el || !el.parentElement) return -1;
575
+ return Array.from(el.parentElement.children).indexOf(el);
567
576
  }
568
577
  const target = (typeof selector === 'string')
569
578
  ? document.querySelector(selector)
@@ -623,6 +632,11 @@ class ZQueryCollection {
623
632
  // --- Attributes ----------------------------------------------------------
624
633
 
625
634
  attr(name, value) {
635
+ if (typeof name === 'object' && name !== null) {
636
+ return this.each((_, el) => {
637
+ for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
638
+ });
639
+ }
626
640
  if (value === undefined) return this.first()?.getAttribute(name);
627
641
  return this.each((_, el) => el.setAttribute(name, value));
628
642
  }
@@ -647,7 +661,10 @@ class ZQueryCollection {
647
661
 
648
662
  // --- CSS / Dimensions ----------------------------------------------------
649
663
 
650
- css(props) {
664
+ css(props, value) {
665
+ if (typeof props === 'string' && value !== undefined) {
666
+ return this.each((_, el) => { el.style[props] = value; });
667
+ }
651
668
  if (typeof props === 'string') {
652
669
  const el = this.first();
653
670
  return el ? getComputedStyle(el)[props] : undefined;
@@ -787,6 +804,7 @@ class ZQueryCollection {
787
804
  wrap(wrapper) {
788
805
  return this.each((_, el) => {
789
806
  const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
807
+ if (!w || !el.parentNode) return;
790
808
  el.parentNode.insertBefore(w, el);
791
809
  w.appendChild(el);
792
810
  });
@@ -912,12 +930,18 @@ class ZQueryCollection {
912
930
  if (typeof selectorOrHandler === 'function') {
913
931
  el.addEventListener(evt, selectorOrHandler);
914
932
  } else if (typeof selectorOrHandler === 'string') {
915
- // Delegated event — only works on elements that support closest()
916
- el.addEventListener(evt, (e) => {
933
+ // Delegated event — store wrapper so off() can remove it
934
+ const wrapper = (e) => {
917
935
  if (!e.target || typeof e.target.closest !== 'function') return;
918
936
  const target = e.target.closest(selectorOrHandler);
919
937
  if (target && el.contains(target)) handler.call(target, e);
920
- });
938
+ };
939
+ wrapper._zqOriginal = handler;
940
+ wrapper._zqSelector = selectorOrHandler;
941
+ el.addEventListener(evt, wrapper);
942
+ // Track delegated handlers for removal
943
+ if (!el._zqDelegated) el._zqDelegated = [];
944
+ el._zqDelegated.push({ evt, wrapper });
921
945
  }
922
946
  });
923
947
  });
@@ -926,7 +950,20 @@ class ZQueryCollection {
926
950
  off(event, handler) {
927
951
  const events = event.split(/\s+/);
928
952
  return this.each((_, el) => {
929
- events.forEach(evt => el.removeEventListener(evt, handler));
953
+ events.forEach(evt => {
954
+ // Try direct removal first
955
+ el.removeEventListener(evt, handler);
956
+ // Also check delegated handlers
957
+ if (el._zqDelegated) {
958
+ el._zqDelegated = el._zqDelegated.filter(d => {
959
+ if (d.evt === evt && d.wrapper._zqOriginal === handler) {
960
+ el.removeEventListener(evt, d.wrapper);
961
+ return false;
962
+ }
963
+ return true;
964
+ });
965
+ }
966
+ });
930
967
  });
931
968
  }
932
969
 
@@ -955,8 +992,12 @@ class ZQueryCollection {
955
992
  // --- Animation -----------------------------------------------------------
956
993
 
957
994
  animate(props, duration = 300, easing = 'ease') {
995
+ // Empty collection — resolve immediately
996
+ if (this.length === 0) return Promise.resolve(this);
958
997
  return new Promise(resolve => {
998
+ let resolved = false;
959
999
  const count = { done: 0 };
1000
+ const listeners = [];
960
1001
  this.each((_, el) => {
961
1002
  el.style.transition = `all ${duration}ms ${easing}`;
962
1003
  requestAnimationFrame(() => {
@@ -964,13 +1005,27 @@ class ZQueryCollection {
964
1005
  const onEnd = () => {
965
1006
  el.removeEventListener('transitionend', onEnd);
966
1007
  el.style.transition = '';
967
- if (++count.done >= this.length) resolve(this);
1008
+ if (!resolved && ++count.done >= this.length) {
1009
+ resolved = true;
1010
+ resolve(this);
1011
+ }
968
1012
  };
969
1013
  el.addEventListener('transitionend', onEnd);
1014
+ listeners.push({ el, onEnd });
970
1015
  });
971
1016
  });
972
1017
  // Fallback in case transitionend doesn't fire
973
- setTimeout(() => resolve(this), duration + 50);
1018
+ setTimeout(() => {
1019
+ if (!resolved) {
1020
+ resolved = true;
1021
+ // Clean up any remaining transitionend listeners
1022
+ for (const { el, onEnd } of listeners) {
1023
+ el.removeEventListener('transitionend', onEnd);
1024
+ el.style.transition = '';
1025
+ }
1026
+ resolve(this);
1027
+ }
1028
+ }, duration + 50);
974
1029
  });
975
1030
  }
976
1031
 
@@ -984,7 +1039,8 @@ class ZQueryCollection {
984
1039
 
985
1040
  fadeToggle(duration = 300) {
986
1041
  return Promise.all(this.elements.map(el => {
987
- const visible = getComputedStyle(el).opacity !== '0' && getComputedStyle(el).display !== 'none';
1042
+ const cs = getComputedStyle(el);
1043
+ const visible = cs.opacity !== '0' && cs.display !== 'none';
988
1044
  const col = new ZQueryCollection([el]);
989
1045
  return visible ? col.fadeOut(duration) : col.fadeIn(duration);
990
1046
  })).then(() => this);
@@ -1780,6 +1836,7 @@ function _isSafeAccess(obj, prop) {
1780
1836
  const BLOCKED = new Set([
1781
1837
  'constructor', '__proto__', 'prototype', '__defineGetter__',
1782
1838
  '__defineSetter__', '__lookupGetter__', '__lookupSetter__',
1839
+ 'call', 'apply', 'bind',
1783
1840
  ]);
1784
1841
  if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
1785
1842
 
@@ -2297,8 +2354,11 @@ function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, new
2297
2354
  if (!lisSet.has(i)) {
2298
2355
  oldParent.insertBefore(oldNode, cursor);
2299
2356
  }
2357
+ // Capture next sibling BEFORE _morphNode — if _morphNode calls
2358
+ // replaceChild, oldNode is removed and nextSibling becomes stale.
2359
+ const nextSib = oldNode.nextSibling;
2300
2360
  _morphNode(oldParent, oldNode, newNode);
2301
- cursor = oldNode.nextSibling;
2361
+ cursor = nextSib;
2302
2362
  } else {
2303
2363
  // Insert new node
2304
2364
  const clone = newNode.cloneNode(true);
@@ -2476,10 +2536,13 @@ function _morphAttributes(oldEl, newEl) {
2476
2536
  }
2477
2537
  }
2478
2538
 
2479
- // Remove stale attributes
2480
- for (let i = oldLen - 1; i >= 0; i--) {
2481
- if (!newNames.has(oldAttrs[i].name)) {
2482
- oldEl.removeAttribute(oldAttrs[i].name);
2539
+ // Remove stale attributes — snapshot names first because oldAttrs
2540
+ // is a live NamedNodeMap that mutates on removeAttribute().
2541
+ const oldNames = new Array(oldLen);
2542
+ for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
2543
+ for (let i = oldNames.length - 1; i >= 0; i--) {
2544
+ if (!newNames.has(oldNames[i])) {
2545
+ oldEl.removeAttribute(oldNames[i]);
2483
2546
  }
2484
2547
  }
2485
2548
  }
@@ -2801,7 +2864,7 @@ class Component {
2801
2864
  if (!watchers) return;
2802
2865
  for (const [key, handler] of Object.entries(watchers)) {
2803
2866
  // Match exact key or parent key (e.g. watcher on 'user' fires when 'user.name' changes)
2804
- if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.') || changedKey === key) {
2867
+ if (changedKey === key || key.startsWith(changedKey + '.') || changedKey.startsWith(key + '.')) {
2805
2868
  const currentVal = _getPath(this.state.__raw || this.state, key);
2806
2869
  const prevVal = this._prevWatchValues?.[key];
2807
2870
  if (currentVal !== prevVal) {
@@ -2950,20 +3013,26 @@ class Component {
2950
3013
  if (!this._mounted && combinedStyles) {
2951
3014
  const scopeAttr = `z-s${this._uid}`;
2952
3015
  this._el.setAttribute(scopeAttr, '');
2953
- let inAtBlock = 0;
3016
+ let noScopeDepth = 0; // brace depth at which a no-scope @-rule started (0 = none active)
3017
+ let braceDepth = 0; // overall brace depth
2954
3018
  const scoped = combinedStyles.replace(/([^{}]+)\{|\}/g, (match, selector) => {
2955
3019
  if (match === '}') {
2956
- if (inAtBlock > 0) inAtBlock--;
3020
+ if (noScopeDepth > 0 && braceDepth <= noScopeDepth) noScopeDepth = 0;
3021
+ braceDepth--;
2957
3022
  return match;
2958
3023
  }
3024
+ braceDepth++;
2959
3025
  const trimmed = selector.trim();
2960
- // Don't scope @-rules (@media, @keyframes, @supports, @container, @layer, @font-face, etc.)
3026
+ // Don't scope @-rules themselves
2961
3027
  if (trimmed.startsWith('@')) {
2962
- inAtBlock++;
3028
+ // @keyframes and @font-face contain non-selector content — skip scoping inside them
3029
+ if (/^@(keyframes|font-face)\b/.test(trimmed)) {
3030
+ noScopeDepth = braceDepth;
3031
+ }
2963
3032
  return match;
2964
3033
  }
2965
- // Don't scope keyframe stops (from, to, 0%, 50%, etc.)
2966
- if (inAtBlock > 0 && /^[\d%\s,fromto]+$/.test(trimmed.replace(/\s/g, ''))) {
3034
+ // Inside @keyframes or @font-face don't scope inner rules
3035
+ if (noScopeDepth > 0 && braceDepth > noScopeDepth) {
2967
3036
  return match;
2968
3037
  }
2969
3038
  return selector.split(',').map(s => `[${scopeAttr}] ${s.trim()}`).join(', ') + ' {';
@@ -3591,6 +3660,21 @@ class Component {
3591
3660
  this._listeners = [];
3592
3661
  this._delegatedEvents = null;
3593
3662
  this._eventBindings = null;
3663
+ // Clear any pending debounce/throttle timers to prevent stale closures.
3664
+ // Timers are keyed by individual child elements, so iterate all descendants.
3665
+ const allEls = this._el.querySelectorAll('*');
3666
+ allEls.forEach(child => {
3667
+ const dTimers = _debounceTimers.get(child);
3668
+ if (dTimers) {
3669
+ for (const key in dTimers) clearTimeout(dTimers[key]);
3670
+ _debounceTimers.delete(child);
3671
+ }
3672
+ const tTimers = _throttleTimers.get(child);
3673
+ if (tTimers) {
3674
+ for (const key in tTimers) clearTimeout(tTimers[key]);
3675
+ _throttleTimers.delete(child);
3676
+ }
3677
+ });
3594
3678
  if (this._styleEl) this._styleEl.remove();
3595
3679
  _instances.delete(this._el);
3596
3680
  this._el.innerHTML = '';
@@ -3886,6 +3970,23 @@ function style(urls, opts = {}) {
3886
3970
  // Unique marker on history.state to identify zQuery-managed entries
3887
3971
  const _ZQ_STATE_KEY = '__zq';
3888
3972
 
3973
+ /**
3974
+ * Shallow-compare two flat objects (for params / query comparison).
3975
+ * Avoids JSON.stringify overhead on every navigation.
3976
+ */
3977
+ function _shallowEqual(a, b) {
3978
+ if (a === b) return true;
3979
+ if (!a || !b) return false;
3980
+ const keysA = Object.keys(a);
3981
+ const keysB = Object.keys(b);
3982
+ if (keysA.length !== keysB.length) return false;
3983
+ for (let i = 0; i < keysA.length; i++) {
3984
+ const k = keysA[i];
3985
+ if (a[k] !== b[k]) return false;
3986
+ }
3987
+ return true;
3988
+ }
3989
+
3889
3990
  class Router {
3890
3991
  constructor(config = {}) {
3891
3992
  this._el = null;
@@ -3933,11 +4034,12 @@ class Router {
3933
4034
  config.routes.forEach(r => this.add(r));
3934
4035
  }
3935
4036
 
3936
- // Listen for navigation
4037
+ // Listen for navigation — store handler references for cleanup in destroy()
3937
4038
  if (this._mode === 'hash') {
3938
- window.addEventListener('hashchange', () => this._resolve());
4039
+ this._onNavEvent = () => this._resolve();
4040
+ window.addEventListener('hashchange', this._onNavEvent);
3939
4041
  } else {
3940
- window.addEventListener('popstate', (e) => {
4042
+ this._onNavEvent = (e) => {
3941
4043
  // Check for substate pop first — if a listener handles it, don't route
3942
4044
  const st = e.state;
3943
4045
  if (st && st[_ZQ_STATE_KEY] === 'substate') {
@@ -3951,11 +4053,12 @@ class Router {
3951
4053
  this._fireSubstate(null, null, 'reset');
3952
4054
  }
3953
4055
  this._resolve();
3954
- });
4056
+ };
4057
+ window.addEventListener('popstate', this._onNavEvent);
3955
4058
  }
3956
4059
 
3957
4060
  // Intercept link clicks for SPA navigation
3958
- document.addEventListener('click', (e) => {
4061
+ this._onLinkClick = (e) => {
3959
4062
  // Don't intercept modified clicks (Ctrl/Cmd+click = new tab)
3960
4063
  if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
3961
4064
  const link = e.target.closest('[z-link]');
@@ -3977,7 +4080,8 @@ class Router {
3977
4080
  const scrollBehavior = link.getAttribute('z-to-top') || 'instant';
3978
4081
  window.scrollTo({ top: 0, behavior: scrollBehavior });
3979
4082
  }
3980
- });
4083
+ };
4084
+ document.addEventListener('click', this._onLinkClick);
3981
4085
 
3982
4086
  // Initial resolve
3983
4087
  if (this._el) {
@@ -4349,8 +4453,8 @@ class Router {
4349
4453
  // with the same params, skip the full destroy/mount cycle and just
4350
4454
  // update props. This prevents flashing and unnecessary DOM churn.
4351
4455
  if (from && this._instance && matched.component === from.route.component) {
4352
- const sameParams = JSON.stringify(params) === JSON.stringify(from.params);
4353
- const sameQuery = JSON.stringify(query) === JSON.stringify(from.query);
4456
+ const sameParams = _shallowEqual(params, from.params);
4457
+ const sameQuery = _shallowEqual(query, from.query);
4354
4458
  if (sameParams && sameQuery) {
4355
4459
  // Identical navigation — nothing to do
4356
4460
  return;
@@ -4447,6 +4551,15 @@ class Router {
4447
4551
  // --- Destroy -------------------------------------------------------------
4448
4552
 
4449
4553
  destroy() {
4554
+ // Remove window/document event listeners to prevent memory leaks
4555
+ if (this._onNavEvent) {
4556
+ window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
4557
+ this._onNavEvent = null;
4558
+ }
4559
+ if (this._onLinkClick) {
4560
+ document.removeEventListener('click', this._onLinkClick);
4561
+ this._onLinkClick = null;
4562
+ }
4450
4563
  if (this._instance) this._instance.destroy();
4451
4564
  this._listeners.clear();
4452
4565
  this._substateListeners = [];
@@ -4509,6 +4622,7 @@ class Store {
4509
4622
  this._getters = config.getters || {};
4510
4623
  this._middleware = [];
4511
4624
  this._history = []; // action log
4625
+ this._maxHistory = config.maxHistory || 1000;
4512
4626
  this._debug = config.debug || false;
4513
4627
 
4514
4628
  // Create reactive state
@@ -4568,6 +4682,10 @@ class Store {
4568
4682
  try {
4569
4683
  const result = action(this.state, ...args);
4570
4684
  this._history.push({ action: name, args, timestamp: Date.now() });
4685
+ // Cap history to prevent unbounded memory growth
4686
+ if (this._history.length > this._maxHistory) {
4687
+ this._history.splice(0, this._history.length - this._maxHistory);
4688
+ }
4571
4689
  return result;
4572
4690
  } catch (err) {
4573
4691
  reportError(ErrorCode.STORE_ACTION, `Action "${name}" threw`, { action: name, args }, err);
@@ -4725,9 +4843,25 @@ async function request(method, url, data, options = {}) {
4725
4843
 
4726
4844
  // Timeout via AbortController
4727
4845
  const controller = new AbortController();
4728
- fetchOpts.signal = options.signal || controller.signal;
4729
4846
  const timeout = options.timeout ?? _config.timeout;
4730
4847
  let timer;
4848
+ // Combine user signal with internal controller for proper timeout support
4849
+ if (options.signal) {
4850
+ // If AbortSignal.any is available, combine both signals
4851
+ if (typeof AbortSignal.any === 'function') {
4852
+ fetchOpts.signal = AbortSignal.any([options.signal, controller.signal]);
4853
+ } else {
4854
+ // Fallback: forward user signal's abort to our controller
4855
+ fetchOpts.signal = controller.signal;
4856
+ if (options.signal.aborted) {
4857
+ controller.abort(options.signal.reason);
4858
+ } else {
4859
+ options.signal.addEventListener('abort', () => controller.abort(options.signal.reason), { once: true });
4860
+ }
4861
+ }
4862
+ } else {
4863
+ fetchOpts.signal = controller.signal;
4864
+ }
4731
4865
  if (timeout > 0) {
4732
4866
  timer = setTimeout(() => controller.abort(), timeout);
4733
4867
  }
@@ -4991,16 +5125,21 @@ function deepClone(obj) {
4991
5125
  * Deep merge objects
4992
5126
  */
4993
5127
  function deepMerge(target, ...sources) {
4994
- for (const source of sources) {
4995
- for (const key of Object.keys(source)) {
4996
- if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
4997
- if (!target[key] || typeof target[key] !== 'object') target[key] = {};
4998
- deepMerge(target[key], source[key]);
5128
+ const seen = new WeakSet();
5129
+ function merge(tgt, src) {
5130
+ if (seen.has(src)) return tgt;
5131
+ seen.add(src);
5132
+ for (const key of Object.keys(src)) {
5133
+ if (src[key] && typeof src[key] === 'object' && !Array.isArray(src[key])) {
5134
+ if (!tgt[key] || typeof tgt[key] !== 'object') tgt[key] = {};
5135
+ merge(tgt[key], src[key]);
4999
5136
  } else {
5000
- target[key] = source[key];
5137
+ tgt[key] = src[key];
5001
5138
  }
5002
5139
  }
5140
+ return tgt;
5003
5141
  }
5142
+ for (const source of sources) merge(target, source);
5004
5143
  return target;
5005
5144
  }
5006
5145
 
@@ -5011,6 +5150,7 @@ function isEqual(a, b) {
5011
5150
  if (a === b) return true;
5012
5151
  if (typeof a !== typeof b) return false;
5013
5152
  if (typeof a !== 'object' || a === null || b === null) return false;
5153
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
5014
5154
  const keysA = Object.keys(a);
5015
5155
  const keysB = Object.keys(b);
5016
5156
  if (keysA.length !== keysB.length) return false;
@@ -5264,8 +5404,8 @@ $.guardCallback = guardCallback;
5264
5404
  $.validate = validate;
5265
5405
 
5266
5406
  // --- Meta ------------------------------------------------------------------
5267
- $.version = '0.9.0';
5268
- $.libSize = '~89 KB';
5407
+ $.version = '0.9.1';
5408
+ $.libSize = '~92 KB';
5269
5409
  $.meta = {}; // populated at build time by CLI bundler
5270
5410
 
5271
5411
  $.noConflict = () => {