@esportsplus/template 0.16.13 → 0.16.15

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 (40) hide show
  1. package/bench/runtime.bench.ts +207 -0
  2. package/build/attributes.js +4 -1
  3. package/build/slot/array.js +50 -4
  4. package/build/slot/render.js +1 -2
  5. package/build/utilities.d.ts +2 -1
  6. package/build/utilities.js +2 -1
  7. package/package.json +5 -5
  8. package/src/attributes.ts +4 -1
  9. package/src/slot/array.ts +76 -4
  10. package/src/slot/render.ts +1 -4
  11. package/src/utilities.ts +3 -1
  12. package/storage/feature-research-2026-03-24.md +475 -0
  13. package/test-output.txt +0 -0
  14. package/{test/attributes.test.ts → tests/attributes.ts} +3 -2
  15. package/tests/compiler/codegen.ts +292 -0
  16. package/tests/compiler/integration.ts +252 -0
  17. package/tests/compiler/ts-parser.ts +160 -0
  18. package/{test/constants.test.ts → tests/constants.ts} +5 -1
  19. package/tests/event/onconnect.ts +147 -0
  20. package/tests/event/onresize.ts +187 -0
  21. package/tests/event/ontick.ts +273 -0
  22. package/{test/slot/array.test.ts → tests/slot/array.ts} +274 -0
  23. package/vitest.bench.config.ts +18 -0
  24. package/vitest.config.ts +1 -1
  25. package/storage/compiler-architecture-2026-01-13.md +0 -420
  26. /package/{test → examples}/index.ts +0 -0
  27. /package/{test → examples}/vite.config.ts +0 -0
  28. /package/{test/compiler/parser.test.ts → tests/compiler/parser.ts} +0 -0
  29. /package/{test/compiler/ts-analyzer.test.ts → tests/compiler/ts-analyzer.ts} +0 -0
  30. /package/{test → tests}/dist/test.js +0 -0
  31. /package/{test → tests}/dist/test.js.map +0 -0
  32. /package/{test/event/index.test.ts → tests/event/index.ts} +0 -0
  33. /package/{test/html.test.ts → tests/html.ts} +0 -0
  34. /package/{test/render.test.ts → tests/render.ts} +0 -0
  35. /package/{test/slot/cleanup.test.ts → tests/slot/cleanup.ts} +0 -0
  36. /package/{test/slot/effect.test.ts → tests/slot/effect.ts} +0 -0
  37. /package/{test/slot/index.test.ts → tests/slot/index.ts} +0 -0
  38. /package/{test/slot/render.test.ts → tests/slot/render.ts} +0 -0
  39. /package/{test/svg.test.ts → tests/svg.ts} +0 -0
  40. /package/{test/utilities.test.ts → tests/utilities.ts} +0 -0
@@ -0,0 +1,273 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { Element } from '../../src/types';
3
+
4
+
5
+ let callbacks: VoidFunction[] = [];
6
+
7
+ vi.mock('../../src/utilities', async (importOriginal) => {
8
+ let original = await importOriginal<typeof import('../../src/utilities')>();
9
+
10
+ return {
11
+ ...original,
12
+ raf: (cb: VoidFunction) => { callbacks.push(cb); }
13
+ };
14
+ });
15
+
16
+
17
+ // Must import after mock setup
18
+ let { add, remove } = await import('../../src/event/ontick');
19
+ let { default: ontick } = await import('../../src/event/ontick');
20
+
21
+
22
+ describe('event/ontick', () => {
23
+ beforeEach(() => {
24
+ callbacks = [];
25
+ });
26
+
27
+ afterEach(() => {
28
+ callbacks = [];
29
+ });
30
+
31
+ function advanceFrame() {
32
+ let current = callbacks.slice();
33
+
34
+ callbacks = [];
35
+
36
+ for (let i = 0, n = current.length; i < n; i++) {
37
+ current[i]();
38
+ }
39
+ }
40
+
41
+ describe('add', () => {
42
+ it('schedules task to run in RAF', () => {
43
+ let called = false,
44
+ task = () => { called = true; };
45
+
46
+ add(task);
47
+ advanceFrame();
48
+
49
+ expect(called).toBe(true);
50
+
51
+ remove(task);
52
+ advanceFrame();
53
+ });
54
+
55
+ it('task runs on each frame while added', () => {
56
+ let count = 0,
57
+ task = () => { count++; };
58
+
59
+ add(task);
60
+
61
+ advanceFrame();
62
+ advanceFrame();
63
+ advanceFrame();
64
+
65
+ expect(count).toBe(3);
66
+
67
+ remove(task);
68
+ advanceFrame();
69
+ });
70
+ });
71
+
72
+ describe('remove', () => {
73
+ it('stops task execution', () => {
74
+ let count = 0,
75
+ task = () => { count++; };
76
+
77
+ add(task);
78
+ advanceFrame();
79
+
80
+ expect(count).toBe(1);
81
+
82
+ remove(task);
83
+ advanceFrame();
84
+
85
+ expect(count).toBe(1);
86
+
87
+ advanceFrame();
88
+ });
89
+
90
+ it('RAF loop stops when no tasks remain', () => {
91
+ let task = () => {};
92
+
93
+ add(task);
94
+ advanceFrame();
95
+ remove(task);
96
+ advanceFrame();
97
+
98
+ // After tick sees no tasks, it should not schedule another raf
99
+ expect(callbacks.length).toBe(0);
100
+ });
101
+ });
102
+
103
+ describe('multiple tasks', () => {
104
+ it('all execute per frame', () => {
105
+ let a = 0,
106
+ b = 0,
107
+ taskA = () => { a++; },
108
+ taskB = () => { b++; };
109
+
110
+ add(taskA);
111
+ add(taskB);
112
+ advanceFrame();
113
+
114
+ expect(a).toBe(1);
115
+ expect(b).toBe(1);
116
+
117
+ remove(taskA);
118
+ remove(taskB);
119
+ advanceFrame();
120
+ });
121
+ });
122
+
123
+ describe('add then remove same task', () => {
124
+ it('no execution after immediate remove', () => {
125
+ let called = false,
126
+ task = () => { called = true; };
127
+
128
+ add(task);
129
+ remove(task);
130
+ advanceFrame();
131
+
132
+ expect(called).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('ontick', () => {
137
+ let container: HTMLElement;
138
+
139
+ beforeEach(() => {
140
+ container = document.createElement('div');
141
+ document.body.appendChild(container);
142
+ });
143
+
144
+ afterEach(() => {
145
+ document.body.removeChild(container);
146
+ });
147
+
148
+ it('calls listener when element is connected', () => {
149
+ let element = document.createElement('div') as unknown as Element,
150
+ called = false,
151
+ storedDispose: VoidFunction | null = null;
152
+
153
+ container.appendChild(element as unknown as Node);
154
+
155
+ ontick(element, (dispose) => { called = true; storedDispose = dispose; });
156
+ advanceFrame();
157
+
158
+ expect(called).toBe(true);
159
+
160
+ storedDispose!();
161
+ advanceFrame();
162
+ });
163
+
164
+ it('listener receives dispose function and element', () => {
165
+ let element = document.createElement('div') as unknown as Element,
166
+ receivedDispose: VoidFunction | null = null,
167
+ receivedElement: unknown = null;
168
+
169
+ container.appendChild(element as unknown as Node);
170
+
171
+ ontick(element, (dispose, el) => {
172
+ receivedDispose = dispose;
173
+ receivedElement = el;
174
+ });
175
+ advanceFrame();
176
+
177
+ expect(typeof receivedDispose).toBe('function');
178
+ expect(receivedElement).toBe(element);
179
+
180
+ receivedDispose!();
181
+ advanceFrame();
182
+ });
183
+
184
+ it('auto-removes when element disconnects', () => {
185
+ let callCount = 0,
186
+ element = document.createElement('div') as unknown as Element;
187
+
188
+ container.appendChild(element as unknown as Node);
189
+
190
+ ontick(element, () => { callCount++; });
191
+ advanceFrame();
192
+
193
+ expect(callCount).toBe(1);
194
+
195
+ container.removeChild(element as unknown as Node);
196
+ advanceFrame();
197
+
198
+ let afterDisconnect = callCount;
199
+
200
+ advanceFrame();
201
+
202
+ expect(callCount).toBe(afterDisconnect);
203
+ });
204
+
205
+ it('retries up to 60 times for connection', () => {
206
+ let called = false,
207
+ element = document.createElement('div') as unknown as Element,
208
+ storedDispose: VoidFunction | null = null;
209
+
210
+ // Not connected to DOM
211
+ ontick(element, (dispose) => { called = true; storedDispose = dispose; });
212
+
213
+ // Advance 59 frames without connecting — should not call or remove
214
+ for (let i = 0; i < 59; i++) {
215
+ advanceFrame();
216
+ }
217
+
218
+ expect(called).toBe(false);
219
+
220
+ // Connect before 60th retry
221
+ container.appendChild(element as unknown as Node);
222
+ advanceFrame();
223
+
224
+ expect(called).toBe(true);
225
+
226
+ storedDispose!();
227
+ advanceFrame();
228
+ });
229
+
230
+ it('removes after 60 retries if never connected', () => {
231
+ let callCount = 0,
232
+ element = document.createElement('div') as unknown as Element;
233
+
234
+ // Not connected to DOM
235
+ ontick(element, () => { callCount++; });
236
+
237
+ // Advance 61 frames — retry=60, counts down each frame, at 0 it removes
238
+ for (let i = 0; i < 62; i++) {
239
+ advanceFrame();
240
+ }
241
+
242
+ expect(callCount).toBe(0);
243
+
244
+ // Verify task was removed — no further calls even if we keep ticking
245
+ advanceFrame();
246
+ advanceFrame();
247
+
248
+ expect(callCount).toBe(0);
249
+ });
250
+
251
+ it('dispose function stops execution', () => {
252
+ let callCount = 0,
253
+ element = document.createElement('div') as unknown as Element,
254
+ storedDispose: VoidFunction | null = null;
255
+
256
+ container.appendChild(element as unknown as Node);
257
+
258
+ ontick(element, (dispose) => {
259
+ callCount++;
260
+ storedDispose = dispose;
261
+ });
262
+
263
+ advanceFrame();
264
+
265
+ expect(callCount).toBe(1);
266
+
267
+ storedDispose!();
268
+ advanceFrame();
269
+
270
+ expect(callCount).toBe(1);
271
+ });
272
+ });
273
+ });
@@ -472,4 +472,278 @@ 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('disconnect cleanup', () => {
691
+ it('cleans up nodes when cleared', async () => {
692
+ let cleanupCount = 0,
693
+ arr = reactive(['a', 'b', 'c'] as string[]),
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
+ let spans = container.querySelectorAll('span');
707
+
708
+ expect(spans.length).toBe(3);
709
+
710
+ // Clear removes all items and their DOM nodes
711
+ arr.splice(0, arr.length);
712
+
713
+ await new Promise(resolve => requestAnimationFrame(resolve));
714
+
715
+ spans = container.querySelectorAll('span');
716
+
717
+ expect(spans.length).toBe(0);
718
+ });
719
+
720
+ it('removes nodes from DOM on pop', async () => {
721
+ let arr = reactive(['a', 'b'] as string[]),
722
+ slot = new ArraySlot(arr, (s) => {
723
+ let frag = document.createDocumentFragment(),
724
+ span = document.createElement('span');
725
+
726
+ span.textContent = s;
727
+ span.setAttribute('data-value', s);
728
+ frag.appendChild(span);
729
+
730
+ return frag as unknown as DocumentFragment;
731
+ });
732
+
733
+ container.appendChild(slot.fragment);
734
+
735
+ let removed = container.querySelector('span[data-value="b"]');
736
+
737
+ expect(removed).not.toBeNull();
738
+
739
+ arr.pop();
740
+
741
+ await new Promise(resolve => requestAnimationFrame(resolve));
742
+
743
+ removed = container.querySelector('span[data-value="b"]');
744
+
745
+ expect(removed).toBeNull();
746
+ expect(container.querySelectorAll('span').length).toBe(1);
747
+ });
748
+ });
475
749
  });
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import path from 'path';
3
+
4
+
5
+ export default defineConfig({
6
+ resolve: {
7
+ alias: {
8
+ '~': path.resolve(__dirname, 'src')
9
+ }
10
+ },
11
+ test: {
12
+ benchmark: {
13
+ include: ['bench/**/*.ts']
14
+ },
15
+ environment: 'jsdom',
16
+ globals: true
17
+ }
18
+ });
package/vitest.config.ts CHANGED
@@ -16,6 +16,6 @@ export default defineConfig({
16
16
  },
17
17
  environment: 'jsdom',
18
18
  globals: true,
19
- include: ['test/**/*.test.ts']
19
+ include: ['tests/**/*.ts']
20
20
  }
21
21
  });