@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
@@ -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
+ });
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
+ });