zero-query 0.9.9 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +34 -33
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +21 -18
  5. package/cli/commands/create.js +9 -2
  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 +3 -3
  18. package/cli/index.js +1 -1
  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 +1 -2
  50. package/cli/scaffold/ssr/app/components/about.js +5 -5
  51. package/cli/scaffold/ssr/app/components/home.js +2 -2
  52. package/cli/scaffold/ssr/app/components/not-found.js +2 -2
  53. package/cli/scaffold/ssr/app/routes.js +1 -1
  54. package/cli/scaffold/ssr/global.css +3 -4
  55. package/cli/scaffold/ssr/index.html +2 -2
  56. package/cli/scaffold/ssr/server/index.js +26 -25
  57. package/cli/utils.js +6 -6
  58. package/dist/zquery.dist.zip +0 -0
  59. package/dist/zquery.js +508 -227
  60. package/dist/zquery.min.js +2 -2
  61. package/index.d.ts +16 -13
  62. package/index.js +7 -5
  63. package/package.json +3 -3
  64. package/src/component.js +64 -63
  65. package/src/core.js +15 -15
  66. package/src/diff.js +38 -38
  67. package/src/errors.js +17 -17
  68. package/src/expression.js +15 -17
  69. package/src/http.js +4 -4
  70. package/src/reactive.js +75 -9
  71. package/src/router.js +104 -24
  72. package/src/ssr.js +28 -28
  73. package/src/store.js +103 -21
  74. package/src/utils.js +64 -12
  75. package/tests/audit.test.js +143 -15
  76. package/tests/cli.test.js +20 -20
  77. package/tests/component.test.js +121 -121
  78. package/tests/core.test.js +56 -56
  79. package/tests/diff.test.js +42 -42
  80. package/tests/errors.test.js +5 -5
  81. package/tests/expression.test.js +58 -53
  82. package/tests/http.test.js +20 -20
  83. package/tests/reactive.test.js +185 -24
  84. package/tests/router.test.js +501 -74
  85. package/tests/ssr.test.js +15 -13
  86. package/tests/store.test.js +264 -23
  87. package/tests/test-minifier.js +153 -0
  88. package/tests/test-ssr.js +27 -0
  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 +3 -3
  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 +2 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { reactive, Signal, signal, computed, effect } from '../src/reactive.js';
2
+ import { reactive, Signal, signal, computed, effect, batch, untracked } from '../src/reactive.js';
3
3
 
4
4
 
5
5
  // ---------------------------------------------------------------------------
@@ -58,7 +58,7 @@ describe('reactive', () => {
58
58
  });
59
59
 
60
60
  it('handles onChange gracefully when onChange is not a function', () => {
61
- // Should not throw error is reported and a no-op is used
61
+ // Should not throw - error is reported and a no-op is used
62
62
  expect(() => {
63
63
  const obj = reactive({ x: 1 }, 'not a function');
64
64
  obj.x = 2;
@@ -231,7 +231,7 @@ describe('effect()', () => {
231
231
  log(toggle.value ? a.value : b.value);
232
232
  });
233
233
  expect(log).toHaveBeenCalledWith('A');
234
- // Change b should NOT re-run because b is not tracked when toggle=true
234
+ // Change b - should NOT re-run because b is not tracked when toggle=true
235
235
  b.value = 'B2';
236
236
  // After toggle switches, b becomes tracked
237
237
  toggle.value = false;
@@ -241,10 +241,10 @@ describe('effect()', () => {
241
241
 
242
242
 
243
243
  // ---------------------------------------------------------------------------
244
- // reactive array mutations
244
+ // reactive - array mutations
245
245
  // ---------------------------------------------------------------------------
246
246
 
247
- describe('reactive arrays', () => {
247
+ describe('reactive - arrays', () => {
248
248
  it('detects push on a reactive array', () => {
249
249
  const fn = vi.fn();
250
250
  const obj = reactive({ items: [1, 2, 3] }, fn);
@@ -262,10 +262,10 @@ describe('reactive — arrays', () => {
262
262
 
263
263
 
264
264
  // ---------------------------------------------------------------------------
265
- // computed advanced
265
+ // computed - advanced
266
266
  // ---------------------------------------------------------------------------
267
267
 
268
- describe('computed advanced', () => {
268
+ describe('computed - advanced', () => {
269
269
  it('chains computed signals', () => {
270
270
  const count = signal(2);
271
271
  const doubled = computed(() => count.value * 2);
@@ -302,10 +302,10 @@ describe('computed — advanced', () => {
302
302
 
303
303
 
304
304
  // ---------------------------------------------------------------------------
305
- // Signal batch behavior
305
+ // Signal - batch behavior
306
306
  // ---------------------------------------------------------------------------
307
307
 
308
- describe('Signal multiple subscribers', () => {
308
+ describe('Signal - multiple subscribers', () => {
309
309
  it('notifies all subscribers', () => {
310
310
  const s = signal(0);
311
311
  const fn1 = vi.fn();
@@ -343,7 +343,7 @@ describe('Signal — multiple subscribers', () => {
343
343
  // BUG FIX: effect() dispose must not corrupt _activeEffect
344
344
  // ---------------------------------------------------------------------------
345
345
 
346
- describe('effect dispose safety', () => {
346
+ describe('effect - dispose safety', () => {
347
347
  it('disposing inside another effect does not break tracking', () => {
348
348
  const a = signal(1);
349
349
  const b = signal(2);
@@ -376,10 +376,10 @@ describe('effect — dispose safety', () => {
376
376
  // PERF FIX: computed() should not notify when value unchanged
377
377
  // ---------------------------------------------------------------------------
378
378
 
379
- describe('computed skip notification on same value', () => {
379
+ describe('computed - skip notification on same value', () => {
380
380
  it('does not notify subscribers when computed result is the same', () => {
381
381
  const s = signal(5);
382
- // Computed that clamps to a range returns same value if within bounds
382
+ // Computed that clamps to a range - returns same value if within bounds
383
383
  const clamped = computed(() => Math.min(Math.max(s.value, 0), 10));
384
384
  expect(clamped.value).toBe(5);
385
385
 
@@ -392,19 +392,19 @@ describe('computed — skip notification on same value', () => {
392
392
  expect(subscriber).toHaveBeenCalledTimes(1);
393
393
 
394
394
  subscriber.mockClear();
395
- // Changing s from 7 to 15 clamped stays at 10
395
+ // Changing s from 7 to 15 - clamped stays at 10
396
396
  s.value = 15;
397
397
  expect(clamped.value).toBe(10);
398
- s.value = 20; // clamped still 10 should NOT notify again
398
+ s.value = 20; // clamped still 10 - should NOT notify again
399
399
  expect(subscriber).toHaveBeenCalledTimes(1); // only the 7→10 change
400
400
  });
401
401
  });
402
402
 
403
403
  // ===========================================================================
404
- // reactive() advanced edge cases
404
+ // reactive() - advanced edge cases
405
405
  // ===========================================================================
406
406
 
407
- describe('reactive edge cases', () => {
407
+ describe('reactive - edge cases', () => {
408
408
  it('returns primitive as-is', () => {
409
409
  expect(reactive(42, () => {})).toBe(42);
410
410
  expect(reactive('hello', () => {})).toBe('hello');
@@ -473,7 +473,7 @@ describe('reactive — edge cases', () => {
473
473
 
474
474
  it('onChange throwing does not prevent set', () => {
475
475
  const r = reactive({ a: 1 }, () => { throw new Error('boom'); });
476
- // Should not throw externally error is reported via reportError
476
+ // Should not throw externally - error is reported via reportError
477
477
  r.a = 2;
478
478
  expect(r.__raw.a).toBe(2);
479
479
  });
@@ -488,10 +488,10 @@ describe('reactive — edge cases', () => {
488
488
 
489
489
 
490
490
  // ===========================================================================
491
- // Signal advanced
491
+ // Signal - advanced
492
492
  // ===========================================================================
493
493
 
494
- describe('Signal advanced', () => {
494
+ describe('Signal - advanced', () => {
495
495
  it('peek() does not trigger tracking', () => {
496
496
  const s = signal(1);
497
497
  const fn = vi.fn(() => { s.peek(); });
@@ -549,10 +549,10 @@ describe('Signal — advanced', () => {
549
549
 
550
550
 
551
551
  // ===========================================================================
552
- // effect() advanced
552
+ // effect() - advanced
553
553
  // ===========================================================================
554
554
 
555
- describe('effect advanced', () => {
555
+ describe('effect - advanced', () => {
556
556
  it('returns dispose function', () => {
557
557
  const s = signal(0);
558
558
  const fn = vi.fn(() => s.value);
@@ -625,17 +625,17 @@ describe('effect — advanced', () => {
625
625
 
626
626
 
627
627
  // ===========================================================================
628
- // computed() advanced
628
+ // computed() - advanced
629
629
  // ===========================================================================
630
630
 
631
- describe('computed advanced', () => {
631
+ describe('computed - advanced', () => {
632
632
  it('computed does not notify when value unchanged', () => {
633
633
  const s = signal(5);
634
634
  const c = computed(() => s.value > 3);
635
635
  const fn = vi.fn();
636
636
  c.subscribe(fn);
637
637
 
638
- s.value = 10; // c still true no change
638
+ s.value = 10; // c still true - no change
639
639
  expect(fn).not.toHaveBeenCalled();
640
640
  });
641
641
 
@@ -656,4 +656,165 @@ describe('computed — advanced', () => {
656
656
  first.value = 'Jane';
657
657
  expect(full.value).toBe('Jane Doe');
658
658
  });
659
+ });
660
+
661
+
662
+ // ===========================================================================
663
+ // batch()
664
+ // ===========================================================================
665
+
666
+ describe('batch()', () => {
667
+ it('defers effect execution until batch completes', () => {
668
+ const a = signal(1);
669
+ const b = signal(2);
670
+ const fn = vi.fn();
671
+
672
+ effect(() => {
673
+ fn(a.value + b.value);
674
+ });
675
+ fn.mockClear(); // clear initial run
676
+
677
+ batch(() => {
678
+ a.value = 10;
679
+ b.value = 20;
680
+ });
681
+
682
+ // Effect should run once after the batch, not twice
683
+ expect(fn).toHaveBeenCalledTimes(1);
684
+ expect(fn).toHaveBeenCalledWith(30);
685
+ });
686
+
687
+ it('subscribers see the final value, not intermediate', () => {
688
+ const s = signal(0);
689
+ const values = [];
690
+ s.subscribe(() => values.push(s.value));
691
+
692
+ batch(() => {
693
+ s.value = 1;
694
+ s.value = 2;
695
+ s.value = 3;
696
+ });
697
+
698
+ expect(values).toEqual([3]);
699
+ });
700
+
701
+ it('nested batch runs inner immediately, flushes at outer', () => {
702
+ const s = signal(0);
703
+ const fn = vi.fn();
704
+ s.subscribe(fn);
705
+
706
+ batch(() => {
707
+ s.value = 1;
708
+ batch(() => {
709
+ s.value = 2;
710
+ });
711
+ // inner batch should not have flushed
712
+ expect(fn).not.toHaveBeenCalled();
713
+ s.value = 3;
714
+ });
715
+
716
+ // Outer batch flushes once
717
+ expect(fn).toHaveBeenCalledTimes(1);
718
+ expect(s.value).toBe(3);
719
+ });
720
+
721
+ it('computed values update correctly after batch', () => {
722
+ const a = signal(1);
723
+ const b = signal(2);
724
+ const sum = computed(() => a.value + b.value);
725
+
726
+ batch(() => {
727
+ a.value = 10;
728
+ b.value = 20;
729
+ });
730
+
731
+ expect(sum.value).toBe(30);
732
+ });
733
+
734
+ it('effects still run if batch throws', () => {
735
+ const s = signal(0);
736
+ const fn = vi.fn();
737
+ s.subscribe(() => fn(s.value));
738
+
739
+ try {
740
+ batch(() => {
741
+ s.value = 42;
742
+ throw new Error('oops');
743
+ });
744
+ } catch {}
745
+
746
+ // Batch should still flush on error via finally
747
+ expect(fn).toHaveBeenCalledWith(42);
748
+ });
749
+ });
750
+
751
+
752
+ // ===========================================================================
753
+ // untracked()
754
+ // ===========================================================================
755
+
756
+ describe('untracked()', () => {
757
+ it('reads signals without creating dependencies', () => {
758
+ const a = signal(1);
759
+ const b = signal(10);
760
+ const fn = vi.fn();
761
+
762
+ effect(() => {
763
+ const aVal = a.value; // tracked
764
+ const bVal = untracked(() => b.value); // not tracked
765
+ fn(aVal + bVal);
766
+ });
767
+ fn.mockClear();
768
+
769
+ // Changing b should NOT re-run the effect
770
+ b.value = 20;
771
+ expect(fn).not.toHaveBeenCalled();
772
+
773
+ // Changing a should re-run and pick up new b
774
+ a.value = 2;
775
+ expect(fn).toHaveBeenCalledTimes(1);
776
+ expect(fn).toHaveBeenCalledWith(22); // a=2 + b=20
777
+ });
778
+
779
+ it('returns the value from the callback', () => {
780
+ const s = signal(42);
781
+ const result = untracked(() => s.value);
782
+ expect(result).toBe(42);
783
+ });
784
+
785
+ it('does not break tracking for outer effect', () => {
786
+ const tracked = signal('hello');
787
+ const notTracked = signal('world');
788
+ const runs = [];
789
+
790
+ effect(() => {
791
+ const t = tracked.value;
792
+ const u = untracked(() => notTracked.value);
793
+ runs.push(`${t} ${u}`);
794
+ });
795
+
796
+ expect(runs).toEqual(['hello world']);
797
+
798
+ tracked.value = 'hi';
799
+ expect(runs).toEqual(['hello world', 'hi world']);
800
+
801
+ notTracked.value = 'earth';
802
+ expect(runs).toEqual(['hello world', 'hi world']); // no re-run
803
+ });
804
+
805
+ it('works inside computed', () => {
806
+ const a = signal(5);
807
+ const b = signal(10);
808
+ const c = computed(() => a.value + untracked(() => b.value));
809
+
810
+ expect(c.value).toBe(15);
811
+
812
+ b.value = 100;
813
+ // computed shouldn't re-evaluate from b change
814
+ expect(c.value).toBe(15);
815
+
816
+ a.value = 1;
817
+ // Now recomputes, picks up new b
818
+ expect(c.value).toBe(101);
819
+ });
659
820
  });