@esportsplus/template 0.16.15 → 0.17.2
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 +81 -9
- package/build/compiler/plugins/vite.d.ts +8 -4
- package/build/compiler/plugins/vite.js +37 -2
- package/build/hmr.d.ts +10 -0
- package/build/hmr.js +42 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/slot/array.js +21 -2
- package/build/slot/effect.d.ts +3 -3
- package/build/slot/effect.js +36 -17
- package/llm.txt +90 -11
- package/package.json +1 -1
- package/src/compiler/plugins/vite.ts +74 -5
- package/src/hmr.ts +70 -0
- package/src/index.ts +1 -0
- package/src/slot/array.ts +30 -2
- package/src/slot/effect.ts +46 -20
- package/tests/attributes.ts +142 -2
- package/tests/compiler/codegen.ts +78 -0
- package/tests/compiler/transform.ts +159 -0
- package/tests/compiler/ts-analyzer.ts +128 -8
- package/tests/compiler/vite-hmr.ts +213 -0
- package/tests/event/index.ts +71 -0
- package/tests/hmr.ts +146 -0
- package/tests/slot/array.ts +201 -0
- package/tests/slot/async.ts +389 -0
- package/tests/slot/effect.ts +88 -1
- package/storage/feature-research-2026-03-24.md +0 -475
- package/test-output.txt +0 -0
package/tests/hmr.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
2
|
+
import { accept, createHotTemplate, hmrReset, modules } from '../src/hmr';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('hmr', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
hmrReset();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('createHotTemplate', () => {
|
|
11
|
+
it('registers a new template and returns a factory', () => {
|
|
12
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<div>hello</div>');
|
|
13
|
+
|
|
14
|
+
expect(typeof factory).toBe('function');
|
|
15
|
+
expect(modules.has('mod1')).toBe(true);
|
|
16
|
+
expect(modules.get('mod1')!.has('tpl1')).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('factory returns a DocumentFragment clone', () => {
|
|
20
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<div>hello</div>');
|
|
21
|
+
let frag = factory();
|
|
22
|
+
|
|
23
|
+
expect(frag).toBeInstanceOf(DocumentFragment);
|
|
24
|
+
expect(frag.firstChild).toBeInstanceOf(HTMLDivElement);
|
|
25
|
+
expect((frag.firstChild as HTMLElement).textContent).toBe('hello');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('factory caches the parsed fragment and returns clones', () => {
|
|
29
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<span>test</span>');
|
|
30
|
+
let frag1 = factory(),
|
|
31
|
+
frag2 = factory();
|
|
32
|
+
|
|
33
|
+
expect(frag1).not.toBe(frag2);
|
|
34
|
+
expect((frag1.firstChild as HTMLElement).tagName).toBe('SPAN');
|
|
35
|
+
expect((frag2.firstChild as HTMLElement).tagName).toBe('SPAN');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns existing factory when called with same moduleId and templateId', () => {
|
|
39
|
+
let factory1 = createHotTemplate('mod1', 'tpl1', '<div>v1</div>'),
|
|
40
|
+
factory2 = createHotTemplate('mod1', 'tpl1', '<div>v2</div>');
|
|
41
|
+
|
|
42
|
+
expect(factory1).toBe(factory2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('updates html when re-registered with same ids', () => {
|
|
46
|
+
createHotTemplate('mod1', 'tpl1', '<div>v1</div>');
|
|
47
|
+
createHotTemplate('mod1', 'tpl1', '<div>v2</div>');
|
|
48
|
+
|
|
49
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<div>v2</div>');
|
|
50
|
+
let frag = factory();
|
|
51
|
+
|
|
52
|
+
expect((frag.firstChild as HTMLElement).textContent).toBe('v2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('invalidates cache when html changes', () => {
|
|
56
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<div>old</div>');
|
|
57
|
+
|
|
58
|
+
factory(); // populate cache
|
|
59
|
+
|
|
60
|
+
createHotTemplate('mod1', 'tpl1', '<div>new</div>');
|
|
61
|
+
|
|
62
|
+
let frag = factory();
|
|
63
|
+
|
|
64
|
+
expect((frag.firstChild as HTMLElement).textContent).toBe('new');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('registers multiple templates per module', () => {
|
|
68
|
+
createHotTemplate('mod1', 'tpl1', '<div>a</div>');
|
|
69
|
+
createHotTemplate('mod1', 'tpl2', '<span>b</span>');
|
|
70
|
+
|
|
71
|
+
expect(modules.get('mod1')!.size).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('registers templates across different modules', () => {
|
|
75
|
+
createHotTemplate('mod1', 'tpl1', '<div>a</div>');
|
|
76
|
+
createHotTemplate('mod2', 'tpl1', '<div>b</div>');
|
|
77
|
+
|
|
78
|
+
expect(modules.size).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('accept', () => {
|
|
83
|
+
it('invalidates all cached templates for a module', () => {
|
|
84
|
+
let factory1 = createHotTemplate('mod1', 'tpl1', '<div>a</div>'),
|
|
85
|
+
factory2 = createHotTemplate('mod1', 'tpl2', '<span>b</span>');
|
|
86
|
+
|
|
87
|
+
factory1(); // populate cache
|
|
88
|
+
factory2(); // populate cache
|
|
89
|
+
|
|
90
|
+
let entry1 = modules.get('mod1')!.get('tpl1')!,
|
|
91
|
+
entry2 = modules.get('mod1')!.get('tpl2')!;
|
|
92
|
+
|
|
93
|
+
expect(entry1.cached).toBeDefined();
|
|
94
|
+
expect(entry2.cached).toBeDefined();
|
|
95
|
+
|
|
96
|
+
accept('mod1');
|
|
97
|
+
|
|
98
|
+
expect(entry1.cached).toBeUndefined();
|
|
99
|
+
expect(entry2.cached).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does not affect templates from other modules', () => {
|
|
103
|
+
let factory1 = createHotTemplate('mod1', 'tpl1', '<div>a</div>'),
|
|
104
|
+
factory2 = createHotTemplate('mod2', 'tpl1', '<div>b</div>');
|
|
105
|
+
|
|
106
|
+
factory1();
|
|
107
|
+
factory2();
|
|
108
|
+
|
|
109
|
+
let entry2 = modules.get('mod2')!.get('tpl1')!;
|
|
110
|
+
|
|
111
|
+
accept('mod1');
|
|
112
|
+
|
|
113
|
+
expect(entry2.cached).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('is a no-op for unknown module ids', () => {
|
|
117
|
+
expect(() => accept('nonexistent')).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('after accept, factory produces new fragments from same html', () => {
|
|
121
|
+
let factory = createHotTemplate('mod1', 'tpl1', '<div>content</div>');
|
|
122
|
+
|
|
123
|
+
let frag1 = factory();
|
|
124
|
+
|
|
125
|
+
accept('mod1');
|
|
126
|
+
|
|
127
|
+
let frag2 = factory();
|
|
128
|
+
|
|
129
|
+
expect(frag1).not.toBe(frag2);
|
|
130
|
+
expect((frag2.firstChild as HTMLElement).textContent).toBe('content');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('hmrReset', () => {
|
|
135
|
+
it('clears all registered modules', () => {
|
|
136
|
+
createHotTemplate('mod1', 'tpl1', '<div>a</div>');
|
|
137
|
+
createHotTemplate('mod2', 'tpl1', '<div>b</div>');
|
|
138
|
+
|
|
139
|
+
expect(modules.size).toBe(2);
|
|
140
|
+
|
|
141
|
+
hmrReset();
|
|
142
|
+
|
|
143
|
+
expect(modules.size).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
package/tests/slot/array.ts
CHANGED
|
@@ -687,6 +687,207 @@ describe('slot/ArraySlot', () => {
|
|
|
687
687
|
});
|
|
688
688
|
});
|
|
689
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
|
+
|
|
690
891
|
describe('disconnect cleanup', () => {
|
|
691
892
|
it('cleans up nodes when cleared', async () => {
|
|
692
893
|
let cleanupCount = 0,
|