@esportsplus/template 0.16.14 → 0.17.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 (55) hide show
  1. package/README.md +33 -1
  2. package/bench/runtime.bench.ts +207 -0
  3. package/build/attributes.js +4 -1
  4. package/build/compiler/plugins/vite.d.ts +8 -4
  5. package/build/compiler/plugins/vite.js +37 -2
  6. package/build/hmr.d.ts +10 -0
  7. package/build/hmr.js +42 -0
  8. package/build/index.d.ts +1 -0
  9. package/build/index.js +1 -0
  10. package/build/slot/array.js +69 -4
  11. package/build/slot/effect.d.ts +3 -3
  12. package/build/slot/effect.js +36 -17
  13. package/build/slot/render.js +1 -2
  14. package/build/utilities.d.ts +2 -1
  15. package/build/utilities.js +2 -1
  16. package/llm.txt +63 -4
  17. package/package.json +2 -1
  18. package/src/attributes.ts +4 -1
  19. package/src/compiler/plugins/vite.ts +74 -5
  20. package/src/hmr.ts +70 -0
  21. package/src/index.ts +1 -0
  22. package/src/slot/array.ts +104 -4
  23. package/src/slot/effect.ts +46 -20
  24. package/src/slot/render.ts +1 -4
  25. package/src/utilities.ts +3 -1
  26. package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
  27. package/tests/compiler/codegen.ts +292 -0
  28. package/tests/compiler/integration.ts +252 -0
  29. package/tests/compiler/ts-parser.ts +160 -0
  30. package/tests/compiler/vite-hmr.ts +126 -0
  31. package/{test/constants.test.ts → tests/constants.ts} +5 -1
  32. package/tests/event/onconnect.ts +147 -0
  33. package/tests/event/onresize.ts +187 -0
  34. package/tests/event/ontick.ts +273 -0
  35. package/tests/hmr.ts +146 -0
  36. package/{test/slot/array.test.ts → tests/slot/array.ts} +475 -0
  37. package/tests/slot/async.ts +389 -0
  38. package/vitest.bench.config.ts +18 -0
  39. package/vitest.config.ts +1 -1
  40. package/storage/compiler-architecture-2026-01-13.md +0 -420
  41. /package/{test → examples}/index.ts +0 -0
  42. /package/{test → examples}/vite.config.ts +0 -0
  43. /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
  44. /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
  45. /package/{test → tests}/dist/test.js +0 -0
  46. /package/{test → tests}/dist/test.js.map +0 -0
  47. /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
  48. /package/{test/html.test.ts → tests/html.ts} +0 -0
  49. /package/{test/render.test.ts → tests/render.ts} +0 -0
  50. /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
  51. /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
  52. /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
  53. /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
  54. /package/{test/svg.test.ts → tests/svg.ts} +0 -0
  55. /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
@@ -472,4 +472,479 @@ describe('slot/ArraySlot', () => {
472
472
  expect(spans[3].textContent).toBe('e');
473
473
  });
474
474
  });
475
+
476
+ describe('rapid successive operations', () => {
477
+ it('batches push+push+pop in same frame', async () => {
478
+ let arr = reactive(['a'] as string[]),
479
+ slot = new ArraySlot(arr, (s) => {
480
+ let frag = document.createDocumentFragment(),
481
+ span = document.createElement('span');
482
+
483
+ span.textContent = s;
484
+ frag.appendChild(span);
485
+
486
+ return frag as unknown as DocumentFragment;
487
+ });
488
+
489
+ container.appendChild(slot.fragment);
490
+
491
+ arr.push('b');
492
+ arr.push('c');
493
+ arr.pop();
494
+
495
+ await new Promise(resolve => requestAnimationFrame(resolve));
496
+
497
+ let spans = container.querySelectorAll('span');
498
+
499
+ expect(spans.length).toBe(2);
500
+ expect(spans[0].textContent).toBe('a');
501
+ expect(spans[1].textContent).toBe('b');
502
+ });
503
+
504
+ it('batches unshift+pop+push in same frame', async () => {
505
+ let arr = reactive(['b'] as string[]),
506
+ slot = new ArraySlot(arr, (s) => {
507
+ let frag = document.createDocumentFragment(),
508
+ span = document.createElement('span');
509
+
510
+ span.textContent = s;
511
+ frag.appendChild(span);
512
+
513
+ return frag as unknown as DocumentFragment;
514
+ });
515
+
516
+ container.appendChild(slot.fragment);
517
+
518
+ arr.unshift('a');
519
+ arr.pop();
520
+ arr.push('c');
521
+
522
+ await new Promise(resolve => requestAnimationFrame(resolve));
523
+
524
+ let spans = container.querySelectorAll('span');
525
+
526
+ expect(spans.length).toBe(2);
527
+ expect(spans[0].textContent).toBe('a');
528
+ expect(spans[1].textContent).toBe('c');
529
+ });
530
+ });
531
+
532
+ describe('empty array edge cases', () => {
533
+ it('pop on empty array does not throw', async () => {
534
+ let arr = reactive([] as string[]),
535
+ slot = new ArraySlot(arr, (s) => {
536
+ let frag = document.createDocumentFragment(),
537
+ span = document.createElement('span');
538
+
539
+ span.textContent = s;
540
+ frag.appendChild(span);
541
+
542
+ return frag as unknown as DocumentFragment;
543
+ });
544
+
545
+ container.appendChild(slot.fragment);
546
+
547
+ arr.pop();
548
+
549
+ await new Promise(resolve => requestAnimationFrame(resolve));
550
+
551
+ let spans = container.querySelectorAll('span');
552
+
553
+ expect(spans.length).toBe(0);
554
+ });
555
+
556
+ it('shift on empty array does not throw', async () => {
557
+ let arr = reactive([] as string[]),
558
+ slot = new ArraySlot(arr, (s) => {
559
+ let frag = document.createDocumentFragment(),
560
+ span = document.createElement('span');
561
+
562
+ span.textContent = s;
563
+ frag.appendChild(span);
564
+
565
+ return frag as unknown as DocumentFragment;
566
+ });
567
+
568
+ container.appendChild(slot.fragment);
569
+
570
+ arr.shift();
571
+
572
+ await new Promise(resolve => requestAnimationFrame(resolve));
573
+
574
+ let spans = container.querySelectorAll('span');
575
+
576
+ expect(spans.length).toBe(0);
577
+ });
578
+
579
+ it('splice beyond bounds does not throw', async () => {
580
+ let arr = reactive(['a'] as string[]),
581
+ slot = new ArraySlot(arr, (s) => {
582
+ let frag = document.createDocumentFragment(),
583
+ span = document.createElement('span');
584
+
585
+ span.textContent = s;
586
+ frag.appendChild(span);
587
+
588
+ return frag as unknown as DocumentFragment;
589
+ });
590
+
591
+ container.appendChild(slot.fragment);
592
+
593
+ arr.splice(10, 5);
594
+
595
+ await new Promise(resolve => requestAnimationFrame(resolve));
596
+
597
+ let spans = container.querySelectorAll('span');
598
+
599
+ expect(spans.length).toBe(1);
600
+ expect(spans[0].textContent).toBe('a');
601
+ });
602
+ });
603
+
604
+ describe('large array operations', () => {
605
+ it('pushes 50+ items and renders all correctly', async () => {
606
+ let arr = reactive([] as number[]),
607
+ slot = new ArraySlot(arr, (n) => {
608
+ let frag = document.createDocumentFragment(),
609
+ span = document.createElement('span');
610
+
611
+ span.textContent = String(n);
612
+ frag.appendChild(span);
613
+
614
+ return frag as unknown as DocumentFragment;
615
+ });
616
+
617
+ container.appendChild(slot.fragment);
618
+
619
+ let items: number[] = [];
620
+
621
+ for (let i = 0; i < 60; i++) {
622
+ items.push(i);
623
+ }
624
+
625
+ arr.push(...items);
626
+
627
+ await new Promise(resolve => requestAnimationFrame(resolve));
628
+
629
+ let spans = container.querySelectorAll('span');
630
+
631
+ expect(spans.length).toBe(60);
632
+ expect(spans[0].textContent).toBe('0');
633
+ expect(spans[59].textContent).toBe('59');
634
+ });
635
+ });
636
+
637
+ describe('set operation', () => {
638
+ it('replaces a single item by index via splice', async () => {
639
+ let arr = reactive(['a', 'b', 'c'] as string[]),
640
+ slot = new ArraySlot(arr, (s) => {
641
+ let frag = document.createDocumentFragment(),
642
+ span = document.createElement('span');
643
+
644
+ span.textContent = s;
645
+ frag.appendChild(span);
646
+
647
+ return frag as unknown as DocumentFragment;
648
+ });
649
+
650
+ container.appendChild(slot.fragment);
651
+
652
+ arr.splice(1, 1, 'x');
653
+
654
+ await new Promise(resolve => requestAnimationFrame(resolve));
655
+
656
+ let spans = container.querySelectorAll('span');
657
+
658
+ expect(spans.length).toBe(3);
659
+ expect(spans[0].textContent).toBe('a');
660
+ expect(spans[1].textContent).toBe('x');
661
+ expect(spans[2].textContent).toBe('c');
662
+ });
663
+
664
+ it('replaces first item by index via splice', async () => {
665
+ let arr = reactive(['a', 'b'] as string[]),
666
+ slot = new ArraySlot(arr, (s) => {
667
+ let frag = document.createDocumentFragment(),
668
+ span = document.createElement('span');
669
+
670
+ span.textContent = s;
671
+ frag.appendChild(span);
672
+
673
+ return frag as unknown as DocumentFragment;
674
+ });
675
+
676
+ container.appendChild(slot.fragment);
677
+
678
+ arr.splice(0, 1, 'z');
679
+
680
+ await new Promise(resolve => requestAnimationFrame(resolve));
681
+
682
+ let spans = container.querySelectorAll('span');
683
+
684
+ expect(spans.length).toBe(2);
685
+ expect(spans[0].textContent).toBe('z');
686
+ expect(spans[1].textContent).toBe('b');
687
+ });
688
+ });
689
+
690
+ describe('moveBefore API', () => {
691
+ it('sort uses moveBefore when available on parent', async () => {
692
+ let arr = reactive(['c', 'a', 'b'] as string[]),
693
+ moveBeforeCalls: [Node, Node | null][] = [],
694
+ slot = new ArraySlot(arr, (s) => {
695
+ let frag = document.createDocumentFragment(),
696
+ span = document.createElement('span');
697
+
698
+ span.textContent = s;
699
+ frag.appendChild(span);
700
+
701
+ return frag as unknown as DocumentFragment;
702
+ });
703
+
704
+ container.appendChild(slot.fragment);
705
+
706
+ // Polyfill moveBefore on the parent
707
+ (container as any).moveBefore = function (node: Node, ref: Node | null) {
708
+ moveBeforeCalls.push([node, ref]);
709
+ container.insertBefore(node, ref);
710
+ };
711
+
712
+ arr.sort();
713
+
714
+ await new Promise(resolve => requestAnimationFrame(resolve));
715
+
716
+ let spans = container.querySelectorAll('span');
717
+
718
+ expect(spans[0].textContent).toBe('a');
719
+ expect(spans[1].textContent).toBe('b');
720
+ expect(spans[2].textContent).toBe('c');
721
+ expect(moveBeforeCalls.length).toBeGreaterThan(0);
722
+
723
+ delete (container as any).moveBefore;
724
+ });
725
+
726
+ it('sort falls back to insertBefore without moveBefore', async () => {
727
+ let arr = reactive(['c', 'a', 'b'] as string[]),
728
+ slot = new ArraySlot(arr, (s) => {
729
+ let frag = document.createDocumentFragment(),
730
+ span = document.createElement('span');
731
+
732
+ span.textContent = s;
733
+ frag.appendChild(span);
734
+
735
+ return frag as unknown as DocumentFragment;
736
+ });
737
+
738
+ container.appendChild(slot.fragment);
739
+
740
+ // Ensure no moveBefore
741
+ expect('moveBefore' in container).toBe(false);
742
+
743
+ arr.sort();
744
+
745
+ await new Promise(resolve => requestAnimationFrame(resolve));
746
+
747
+ let spans = container.querySelectorAll('span');
748
+
749
+ expect(spans[0].textContent).toBe('a');
750
+ expect(spans[1].textContent).toBe('b');
751
+ expect(spans[2].textContent).toBe('c');
752
+ });
753
+
754
+ it('reverse uses moveBefore when available via sync', async () => {
755
+ let arr = reactive(['a', 'b', 'c'] as string[]),
756
+ moveBeforeCalls: [Node, Node | null][] = [],
757
+ slot = new ArraySlot(arr, (s) => {
758
+ let frag = document.createDocumentFragment(),
759
+ span = document.createElement('span');
760
+
761
+ span.textContent = s;
762
+ frag.appendChild(span);
763
+
764
+ return frag as unknown as DocumentFragment;
765
+ });
766
+
767
+ container.appendChild(slot.fragment);
768
+
769
+ (container as any).moveBefore = function (node: Node, ref: Node | null) {
770
+ moveBeforeCalls.push([node, ref]);
771
+ container.insertBefore(node, ref);
772
+ };
773
+
774
+ arr.reverse();
775
+
776
+ await new Promise(resolve => requestAnimationFrame(resolve));
777
+
778
+ let spans = container.querySelectorAll('span');
779
+
780
+ expect(spans[0].textContent).toBe('c');
781
+ expect(spans[1].textContent).toBe('b');
782
+ expect(spans[2].textContent).toBe('a');
783
+ expect(moveBeforeCalls.length).toBeGreaterThan(0);
784
+
785
+ delete (container as any).moveBefore;
786
+ });
787
+
788
+ it('reverse falls back to fragment approach without moveBefore', async () => {
789
+ let arr = reactive(['a', 'b', 'c'] as string[]),
790
+ slot = new ArraySlot(arr, (s) => {
791
+ let frag = document.createDocumentFragment(),
792
+ span = document.createElement('span');
793
+
794
+ span.textContent = s;
795
+ frag.appendChild(span);
796
+
797
+ return frag as unknown as DocumentFragment;
798
+ });
799
+
800
+ container.appendChild(slot.fragment);
801
+
802
+ expect('moveBefore' in container).toBe(false);
803
+
804
+ arr.reverse();
805
+
806
+ await new Promise(resolve => requestAnimationFrame(resolve));
807
+
808
+ let spans = container.querySelectorAll('span');
809
+
810
+ expect(spans[0].textContent).toBe('c');
811
+ expect(spans[1].textContent).toBe('b');
812
+ expect(spans[2].textContent).toBe('a');
813
+ });
814
+
815
+ it('moveBefore receives correct arguments during sort', async () => {
816
+ let arr = reactive(['b', 'a'] as string[]),
817
+ moveBeforeCalls: [string, string | null][] = [],
818
+ slot = new ArraySlot(arr, (s) => {
819
+ let frag = document.createDocumentFragment(),
820
+ span = document.createElement('span');
821
+
822
+ span.textContent = s;
823
+ span.setAttribute('data-id', s);
824
+ frag.appendChild(span);
825
+
826
+ return frag as unknown as DocumentFragment;
827
+ });
828
+
829
+ container.appendChild(slot.fragment);
830
+
831
+ (container as any).moveBefore = function (node: Node, ref: Node | null) {
832
+ let nodeId = (node as HTMLElement).getAttribute?.('data-id') || '?',
833
+ refId = ref ? ((ref as HTMLElement).getAttribute?.('data-id') || '?') : null;
834
+
835
+ moveBeforeCalls.push([nodeId, refId]);
836
+ container.insertBefore(node, ref);
837
+ };
838
+
839
+ arr.sort();
840
+
841
+ await new Promise(resolve => requestAnimationFrame(resolve));
842
+
843
+ let spans = container.querySelectorAll('span');
844
+
845
+ expect(spans[0].textContent).toBe('a');
846
+ expect(spans[1].textContent).toBe('b');
847
+
848
+ // 'a' should be moved before 'b'
849
+ expect(moveBeforeCalls.some(([n]) => n === 'a')).toBe(true);
850
+
851
+ delete (container as any).moveBefore;
852
+ });
853
+
854
+ it('sort with moveBefore preserves node order for already-sorted LIS', async () => {
855
+ let arr = reactive(['a', 'b', 'c'] as string[]),
856
+ moveBeforeCalls: [Node, Node | null][] = [],
857
+ slot = new ArraySlot(arr, (s) => {
858
+ let frag = document.createDocumentFragment(),
859
+ span = document.createElement('span');
860
+
861
+ span.textContent = s;
862
+ frag.appendChild(span);
863
+
864
+ return frag as unknown as DocumentFragment;
865
+ });
866
+
867
+ container.appendChild(slot.fragment);
868
+
869
+ (container as any).moveBefore = function (node: Node, ref: Node | null) {
870
+ moveBeforeCalls.push([node, ref]);
871
+ container.insertBefore(node, ref);
872
+ };
873
+
874
+ arr.sort();
875
+
876
+ await new Promise(resolve => requestAnimationFrame(resolve));
877
+
878
+ let spans = container.querySelectorAll('span');
879
+
880
+ expect(spans[0].textContent).toBe('a');
881
+ expect(spans[1].textContent).toBe('b');
882
+ expect(spans[2].textContent).toBe('c');
883
+
884
+ // Already sorted — LIS covers all nodes, no moves needed
885
+ expect(moveBeforeCalls.length).toBe(0);
886
+
887
+ delete (container as any).moveBefore;
888
+ });
889
+ });
890
+
891
+ describe('disconnect cleanup', () => {
892
+ it('cleans up nodes when cleared', async () => {
893
+ let cleanupCount = 0,
894
+ arr = reactive(['a', 'b', 'c'] as string[]),
895
+ slot = new ArraySlot(arr, (s) => {
896
+ let frag = document.createDocumentFragment(),
897
+ span = document.createElement('span');
898
+
899
+ span.textContent = s;
900
+ frag.appendChild(span);
901
+
902
+ return frag as unknown as DocumentFragment;
903
+ });
904
+
905
+ container.appendChild(slot.fragment);
906
+
907
+ let spans = container.querySelectorAll('span');
908
+
909
+ expect(spans.length).toBe(3);
910
+
911
+ // Clear removes all items and their DOM nodes
912
+ arr.splice(0, arr.length);
913
+
914
+ await new Promise(resolve => requestAnimationFrame(resolve));
915
+
916
+ spans = container.querySelectorAll('span');
917
+
918
+ expect(spans.length).toBe(0);
919
+ });
920
+
921
+ it('removes nodes from DOM on pop', async () => {
922
+ let arr = reactive(['a', 'b'] as string[]),
923
+ slot = new ArraySlot(arr, (s) => {
924
+ let frag = document.createDocumentFragment(),
925
+ span = document.createElement('span');
926
+
927
+ span.textContent = s;
928
+ span.setAttribute('data-value', s);
929
+ frag.appendChild(span);
930
+
931
+ return frag as unknown as DocumentFragment;
932
+ });
933
+
934
+ container.appendChild(slot.fragment);
935
+
936
+ let removed = container.querySelector('span[data-value="b"]');
937
+
938
+ expect(removed).not.toBeNull();
939
+
940
+ arr.pop();
941
+
942
+ await new Promise(resolve => requestAnimationFrame(resolve));
943
+
944
+ removed = container.querySelector('span[data-value="b"]');
945
+
946
+ expect(removed).toBeNull();
947
+ expect(container.querySelectorAll('span').length).toBe(1);
948
+ });
949
+ });
475
950
  });