@esportsplus/template 0.16.0 → 0.16.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.
Files changed (53) hide show
  1. package/.editorconfig +9 -9
  2. package/.gitattributes +2 -2
  3. package/.github/dependabot.yml +24 -24
  4. package/.github/workflows/bump.yml +8 -8
  5. package/.github/workflows/dependabot.yml +11 -11
  6. package/.github/workflows/publish.yml +16 -16
  7. package/README.md +385 -385
  8. package/build/compiler/codegen.js +9 -9
  9. package/build/compiler/index.js +3 -3
  10. package/{src/llm.txt → llm.txt} +403 -403
  11. package/package.json +10 -3
  12. package/src/attributes.ts +312 -312
  13. package/src/compiler/codegen.ts +492 -492
  14. package/src/compiler/constants.ts +24 -24
  15. package/src/compiler/index.ts +87 -87
  16. package/src/compiler/parser.ts +242 -242
  17. package/src/compiler/plugins/tsc.ts +6 -6
  18. package/src/compiler/plugins/vite.ts +10 -10
  19. package/src/compiler/ts-analyzer.ts +89 -89
  20. package/src/compiler/ts-parser.ts +112 -112
  21. package/src/constants.ts +44 -44
  22. package/src/event/index.ts +130 -130
  23. package/src/event/onconnect.ts +22 -22
  24. package/src/event/onresize.ts +37 -37
  25. package/src/event/ontick.ts +59 -59
  26. package/src/html.ts +18 -18
  27. package/src/index.ts +18 -18
  28. package/src/render.ts +13 -13
  29. package/src/slot/array.ts +257 -257
  30. package/src/slot/cleanup.ts +37 -37
  31. package/src/slot/effect.ts +114 -114
  32. package/src/slot/index.ts +16 -16
  33. package/src/slot/render.ts +61 -61
  34. package/src/svg.ts +27 -27
  35. package/src/types.ts +40 -40
  36. package/src/utilities.ts +53 -53
  37. package/test/attributes.test.ts +311 -0
  38. package/test/compiler/parser.test.ts +402 -0
  39. package/test/compiler/ts-analyzer.test.ts +296 -0
  40. package/test/constants.test.ts +153 -0
  41. package/test/event/index.test.ts +359 -0
  42. package/test/html.test.ts +33 -0
  43. package/test/index.ts +648 -648
  44. package/test/render.test.ts +154 -0
  45. package/test/slot/array.test.ts +475 -0
  46. package/test/slot/cleanup.test.ts +243 -0
  47. package/test/slot/effect.test.ts +263 -0
  48. package/test/slot/index.test.ts +176 -0
  49. package/test/slot/render.test.ts +216 -0
  50. package/test/svg.test.ts +92 -0
  51. package/test/utilities.test.ts +242 -0
  52. package/tsconfig.json +8 -8
  53. package/vitest.config.ts +21 -0
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ ARRAY_SLOT,
4
+ ATTRIBUTE_DELIMITERS,
5
+ CLEANUP,
6
+ DIRECT_ATTACH_EVENTS,
7
+ LIFECYCLE_EVENTS,
8
+ PACKAGE_NAME,
9
+ SLOT_HTML,
10
+ STATE_HYDRATING,
11
+ STATE_NONE,
12
+ STATE_WAITING,
13
+ STORE
14
+ } from '../src/constants';
15
+
16
+
17
+ describe('constants', () => {
18
+ describe('Symbols', () => {
19
+ it('ARRAY_SLOT is a Symbol', () => {
20
+ expect(typeof ARRAY_SLOT).toBe('symbol');
21
+ });
22
+
23
+ it('CLEANUP is a Symbol', () => {
24
+ expect(typeof CLEANUP).toBe('symbol');
25
+ });
26
+
27
+ it('STORE is a Symbol', () => {
28
+ expect(typeof STORE).toBe('symbol');
29
+ });
30
+
31
+ it('Symbols are unique', () => {
32
+ expect(ARRAY_SLOT).not.toBe(CLEANUP);
33
+ expect(ARRAY_SLOT).not.toBe(STORE);
34
+ expect(CLEANUP).not.toBe(STORE);
35
+ });
36
+ });
37
+
38
+ describe('ATTRIBUTE_DELIMITERS', () => {
39
+ it('has class delimiter as space', () => {
40
+ expect(ATTRIBUTE_DELIMITERS.class).toBe(' ');
41
+ });
42
+
43
+ it('has style delimiter as semicolon', () => {
44
+ expect(ATTRIBUTE_DELIMITERS.style).toBe(';');
45
+ });
46
+ });
47
+
48
+ describe('DIRECT_ATTACH_EVENTS', () => {
49
+ it('is a Set', () => {
50
+ expect(DIRECT_ATTACH_EVENTS).toBeInstanceOf(Set);
51
+ });
52
+
53
+ it('contains blur events', () => {
54
+ expect(DIRECT_ATTACH_EVENTS.has('onblur')).toBe(true);
55
+ });
56
+
57
+ it('contains focus events', () => {
58
+ expect(DIRECT_ATTACH_EVENTS.has('onfocus')).toBe(true);
59
+ expect(DIRECT_ATTACH_EVENTS.has('onfocusin')).toBe(true);
60
+ expect(DIRECT_ATTACH_EVENTS.has('onfocusout')).toBe(true);
61
+ });
62
+
63
+ it('contains media events', () => {
64
+ expect(DIRECT_ATTACH_EVENTS.has('onplay')).toBe(true);
65
+ expect(DIRECT_ATTACH_EVENTS.has('onpause')).toBe(true);
66
+ expect(DIRECT_ATTACH_EVENTS.has('onended')).toBe(true);
67
+ expect(DIRECT_ATTACH_EVENTS.has('ontimeupdate')).toBe(true);
68
+ });
69
+
70
+ it('contains form events', () => {
71
+ expect(DIRECT_ATTACH_EVENTS.has('onsubmit')).toBe(true);
72
+ expect(DIRECT_ATTACH_EVENTS.has('onreset')).toBe(true);
73
+ });
74
+
75
+ it('contains error and load events', () => {
76
+ expect(DIRECT_ATTACH_EVENTS.has('onerror')).toBe(true);
77
+ expect(DIRECT_ATTACH_EVENTS.has('onload')).toBe(true);
78
+ });
79
+
80
+ it('contains scroll event', () => {
81
+ expect(DIRECT_ATTACH_EVENTS.has('onscroll')).toBe(true);
82
+ });
83
+
84
+ it('does not contain delegatable events', () => {
85
+ expect(DIRECT_ATTACH_EVENTS.has('onclick')).toBe(false);
86
+ expect(DIRECT_ATTACH_EVENTS.has('onkeydown')).toBe(false);
87
+ expect(DIRECT_ATTACH_EVENTS.has('onmouseenter')).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe('LIFECYCLE_EVENTS', () => {
92
+ it('is a Set', () => {
93
+ expect(LIFECYCLE_EVENTS).toBeInstanceOf(Set);
94
+ });
95
+
96
+ it('contains onconnect', () => {
97
+ expect(LIFECYCLE_EVENTS.has('onconnect')).toBe(true);
98
+ });
99
+
100
+ it('contains ondisconnect', () => {
101
+ expect(LIFECYCLE_EVENTS.has('ondisconnect')).toBe(true);
102
+ });
103
+
104
+ it('contains onrender', () => {
105
+ expect(LIFECYCLE_EVENTS.has('onrender')).toBe(true);
106
+ });
107
+
108
+ it('contains onresize', () => {
109
+ expect(LIFECYCLE_EVENTS.has('onresize')).toBe(true);
110
+ });
111
+
112
+ it('contains ontick', () => {
113
+ expect(LIFECYCLE_EVENTS.has('ontick')).toBe(true);
114
+ });
115
+
116
+ it('does not contain standard DOM events', () => {
117
+ expect(LIFECYCLE_EVENTS.has('onclick')).toBe(false);
118
+ expect(LIFECYCLE_EVENTS.has('onsubmit')).toBe(false);
119
+ });
120
+ });
121
+
122
+ describe('PACKAGE_NAME', () => {
123
+ it('is @esportsplus/template', () => {
124
+ expect(PACKAGE_NAME).toBe('@esportsplus/template');
125
+ });
126
+ });
127
+
128
+ describe('SLOT_HTML', () => {
129
+ it('is a comment marker', () => {
130
+ expect(SLOT_HTML).toBe('<!--$-->');
131
+ });
132
+ });
133
+
134
+ describe('State constants', () => {
135
+ it('STATE_HYDRATING is 0', () => {
136
+ expect(STATE_HYDRATING).toBe(0);
137
+ });
138
+
139
+ it('STATE_NONE is 1', () => {
140
+ expect(STATE_NONE).toBe(1);
141
+ });
142
+
143
+ it('STATE_WAITING is 2', () => {
144
+ expect(STATE_WAITING).toBe(2);
145
+ });
146
+
147
+ it('states are distinct', () => {
148
+ expect(STATE_HYDRATING).not.toBe(STATE_NONE);
149
+ expect(STATE_HYDRATING).not.toBe(STATE_WAITING);
150
+ expect(STATE_NONE).not.toBe(STATE_WAITING);
151
+ });
152
+ });
153
+ });
@@ -0,0 +1,359 @@
1
+ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
2
+ import { delegate, on, ondisconnect, onrender, runtime } from '../../src/event';
3
+ import { CLEANUP } from '../../src/constants';
4
+ import type { Element } from '../../src/types';
5
+
6
+
7
+ describe('event/index', () => {
8
+ let container: HTMLElement;
9
+
10
+ beforeEach(() => {
11
+ container = document.createElement('div');
12
+ document.body.appendChild(container);
13
+ });
14
+
15
+ afterEach(() => {
16
+ document.body.removeChild(container);
17
+ });
18
+
19
+ describe('delegate', () => {
20
+ it('registers click handler via delegation', () => {
21
+ let element = document.createElement('button') as Element,
22
+ clicked = false;
23
+
24
+ container.appendChild(element as unknown as Node);
25
+ delegate(element, 'click', () => { clicked = true; });
26
+
27
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
28
+
29
+ expect(clicked).toBe(true);
30
+ });
31
+
32
+ it('handler receives event object', () => {
33
+ let element = document.createElement('button') as Element,
34
+ receivedEvent: Event | null = null;
35
+
36
+ container.appendChild(element as unknown as Node);
37
+ delegate(element, 'click', (e) => { receivedEvent = e; });
38
+
39
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
40
+
41
+ expect(receivedEvent).toBeInstanceOf(MouseEvent);
42
+ });
43
+
44
+ it('handler is called with element as this', () => {
45
+ let element = document.createElement('button') as Element,
46
+ thisValue: unknown = null;
47
+
48
+ container.appendChild(element as unknown as Node);
49
+ delegate(element, 'click', function(this: unknown) { thisValue = this; });
50
+
51
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
52
+
53
+ expect(thisValue).toBe(element);
54
+ });
55
+
56
+ it('event bubbles up to find handler', () => {
57
+ let parent = document.createElement('div') as Element,
58
+ child = document.createElement('span'),
59
+ clicked = false;
60
+
61
+ parent.appendChild(child);
62
+ container.appendChild(parent as unknown as Node);
63
+
64
+ delegate(parent, 'click', () => { clicked = true; });
65
+
66
+ child.dispatchEvent(new MouseEvent('click', { bubbles: true }));
67
+
68
+ expect(clicked).toBe(true);
69
+ });
70
+
71
+ it('supports multiple different event types', () => {
72
+ let element = document.createElement('div') as Element,
73
+ clicked = false,
74
+ mousedown = false;
75
+
76
+ container.appendChild(element as unknown as Node);
77
+
78
+ delegate(element, 'click', () => { clicked = true; });
79
+ delegate(element, 'mousedown', () => { mousedown = true; });
80
+
81
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
82
+
83
+ expect(clicked).toBe(true);
84
+ expect(mousedown).toBe(false);
85
+
86
+ element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
87
+
88
+ expect(mousedown).toBe(true);
89
+ });
90
+
91
+ it('later handler replaces earlier handler for same event', () => {
92
+ let element = document.createElement('button') as Element,
93
+ firstCalled = false,
94
+ secondCalled = false;
95
+
96
+ container.appendChild(element as unknown as Node);
97
+
98
+ delegate(element, 'click', () => { firstCalled = true; });
99
+ delegate(element, 'click', () => { secondCalled = true; });
100
+
101
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
102
+
103
+ expect(firstCalled).toBe(false);
104
+ expect(secondCalled).toBe(true);
105
+ });
106
+ });
107
+
108
+ describe('on (direct attachment)', () => {
109
+ it('attaches event listener directly', () => {
110
+ let element = document.createElement('input') as Element,
111
+ focused = false;
112
+
113
+ container.appendChild(element as unknown as Node);
114
+ on(element, 'focus', () => { focused = true; });
115
+
116
+ element.dispatchEvent(new FocusEvent('focus'));
117
+
118
+ expect(focused).toBe(true);
119
+ });
120
+
121
+ it('handler receives event object', () => {
122
+ let element = document.createElement('input') as Element,
123
+ receivedEvent: Event | null = null;
124
+
125
+ container.appendChild(element as unknown as Node);
126
+ on(element, 'blur', (e) => { receivedEvent = e; });
127
+
128
+ element.dispatchEvent(new FocusEvent('blur'));
129
+
130
+ expect(receivedEvent).toBeInstanceOf(FocusEvent);
131
+ });
132
+
133
+ it('handler is called with element as this', () => {
134
+ let element = document.createElement('input') as Element,
135
+ thisValue: unknown = null;
136
+
137
+ container.appendChild(element as unknown as Node);
138
+ on(element, 'focus', function(this: unknown) { thisValue = this; });
139
+
140
+ element.dispatchEvent(new FocusEvent('focus'));
141
+
142
+ expect(thisValue).toBe(element);
143
+ });
144
+
145
+ it('registers cleanup function for removal', () => {
146
+ let element = document.createElement('input') as HTMLElement & { [key: symbol]: unknown };
147
+
148
+ container.appendChild(element);
149
+ on(element as unknown as Element, 'focus', () => {});
150
+
151
+ expect(element[CLEANUP]).toBeInstanceOf(Array);
152
+ expect((element[CLEANUP] as unknown[]).length).toBeGreaterThan(0);
153
+ });
154
+ });
155
+
156
+ describe('ondisconnect', () => {
157
+ it('registers disconnect callback', () => {
158
+ let element = document.createElement('div') as HTMLElement & { [key: symbol]: unknown },
159
+ callback = vi.fn();
160
+
161
+ container.appendChild(element);
162
+ ondisconnect(element as unknown as Element, callback);
163
+
164
+ expect(element[CLEANUP]).toBeInstanceOf(Array);
165
+ });
166
+
167
+ it('callback receives element when invoked', () => {
168
+ let element = document.createElement('div') as HTMLElement & { [key: symbol]: unknown },
169
+ receivedElement: unknown = null;
170
+
171
+ container.appendChild(element);
172
+ ondisconnect(element as unknown as Element, (el) => { receivedElement = el; });
173
+
174
+ // Manually trigger cleanup
175
+ let fns = element[CLEANUP] as VoidFunction[];
176
+
177
+ fns[0]();
178
+
179
+ expect(receivedElement).toBe(element);
180
+ });
181
+ });
182
+
183
+ describe('onrender', () => {
184
+ it('calls listener immediately with element', () => {
185
+ let element = document.createElement('div') as Element,
186
+ receivedElement: unknown = null;
187
+
188
+ container.appendChild(element as unknown as Node);
189
+ onrender(element, (el) => { receivedElement = el; });
190
+
191
+ expect(receivedElement).toBe(element);
192
+ });
193
+
194
+ it('calls listener synchronously', () => {
195
+ let element = document.createElement('div') as Element,
196
+ callOrder: string[] = [];
197
+
198
+ container.appendChild(element as unknown as Node);
199
+
200
+ callOrder.push('before');
201
+ onrender(element, () => { callOrder.push('render'); });
202
+ callOrder.push('after');
203
+
204
+ expect(callOrder).toEqual(['before', 'render', 'after']);
205
+ });
206
+ });
207
+
208
+ describe('runtime', () => {
209
+ it('routes click event to delegate', () => {
210
+ let element = document.createElement('button') as Element,
211
+ clicked = false;
212
+
213
+ container.appendChild(element as unknown as Node);
214
+ runtime(element, 'onclick', () => { clicked = true; });
215
+
216
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
217
+
218
+ expect(clicked).toBe(true);
219
+ });
220
+
221
+ it('routes focus event to on (direct attach)', () => {
222
+ let element = document.createElement('input') as Element,
223
+ focused = false;
224
+
225
+ container.appendChild(element as unknown as Node);
226
+ runtime(element, 'onfocus', () => { focused = true; });
227
+
228
+ element.dispatchEvent(new FocusEvent('focus'));
229
+
230
+ expect(focused).toBe(true);
231
+ });
232
+
233
+ it('routes blur event to on (direct attach)', () => {
234
+ let element = document.createElement('input') as Element,
235
+ blurred = false;
236
+
237
+ container.appendChild(element as unknown as Node);
238
+ runtime(element, 'onblur', () => { blurred = true; });
239
+
240
+ element.dispatchEvent(new FocusEvent('blur'));
241
+
242
+ expect(blurred).toBe(true);
243
+ });
244
+
245
+ it('routes onrender to lifecycle handler', () => {
246
+ let element = document.createElement('div') as Element,
247
+ rendered = false;
248
+
249
+ container.appendChild(element as unknown as Node);
250
+ runtime(element, 'onrender', () => { rendered = true; });
251
+
252
+ expect(rendered).toBe(true);
253
+ });
254
+
255
+ it('routes ondisconnect to lifecycle handler', () => {
256
+ let element = document.createElement('div') as HTMLElement & { [key: symbol]: unknown };
257
+
258
+ container.appendChild(element);
259
+ runtime(element as unknown as Element, 'ondisconnect', () => {});
260
+
261
+ expect(element[CLEANUP]).toBeInstanceOf(Array);
262
+ });
263
+
264
+ it('handles case insensitive event names', () => {
265
+ let element = document.createElement('button') as Element,
266
+ clicked = false;
267
+
268
+ container.appendChild(element as unknown as Node);
269
+ runtime(element, 'onClick', () => { clicked = true; });
270
+
271
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
272
+
273
+ expect(clicked).toBe(true);
274
+ });
275
+
276
+ it('routes scroll to direct attach', () => {
277
+ let element = document.createElement('div') as Element,
278
+ scrolled = false;
279
+
280
+ container.appendChild(element as unknown as Node);
281
+ runtime(element, 'onscroll', () => { scrolled = true; });
282
+
283
+ element.dispatchEvent(new Event('scroll'));
284
+
285
+ expect(scrolled).toBe(true);
286
+ });
287
+
288
+ it('routes submit to direct attach', () => {
289
+ let form = document.createElement('form') as Element,
290
+ submitted = false;
291
+
292
+ container.appendChild(form as unknown as Node);
293
+ runtime(form, 'onsubmit', (e) => {
294
+ e.preventDefault();
295
+ submitted = true;
296
+ });
297
+
298
+ form.dispatchEvent(new Event('submit'));
299
+
300
+ expect(submitted).toBe(true);
301
+ });
302
+ });
303
+
304
+ describe('event delegation storage', () => {
305
+ it('stores handler on element via symbol key', () => {
306
+ let element = document.createElement('button') as Element,
307
+ handler = () => {};
308
+
309
+ container.appendChild(element as unknown as Node);
310
+ delegate(element, 'click', handler);
311
+
312
+ // Handler is stored on element - check it can be invoked
313
+ let clicked = false;
314
+
315
+ delegate(element, 'click', () => { clicked = true; });
316
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
317
+
318
+ expect(clicked).toBe(true);
319
+ });
320
+ });
321
+
322
+ describe('passive events', () => {
323
+ it('wheel event uses passive listener', () => {
324
+ let element = document.createElement('div') as Element,
325
+ wheeled = false;
326
+
327
+ container.appendChild(element as unknown as Node);
328
+ delegate(element, 'wheel', () => { wheeled = true; });
329
+
330
+ element.dispatchEvent(new WheelEvent('wheel', { bubbles: true }));
331
+
332
+ expect(wheeled).toBe(true);
333
+ });
334
+
335
+ it('touchstart event uses passive listener', () => {
336
+ let element = document.createElement('div') as Element,
337
+ touched = false;
338
+
339
+ container.appendChild(element as unknown as Node);
340
+ delegate(element, 'touchstart', () => { touched = true; });
341
+
342
+ element.dispatchEvent(new TouchEvent('touchstart', { bubbles: true }));
343
+
344
+ expect(touched).toBe(true);
345
+ });
346
+
347
+ it('scroll event (direct attach) uses passive', () => {
348
+ let element = document.createElement('div') as Element,
349
+ scrolled = false;
350
+
351
+ container.appendChild(element as unknown as Node);
352
+ on(element, 'scroll', () => { scrolled = true; });
353
+
354
+ element.dispatchEvent(new Event('scroll'));
355
+
356
+ expect(scrolled).toBe(true);
357
+ });
358
+ });
359
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import html from '../src/html';
3
+
4
+
5
+ describe('html', () => {
6
+ describe('html tagged template', () => {
7
+ it('is a function', () => {
8
+ expect(typeof html).toBe('function');
9
+ });
10
+
11
+ it('throws when called without compilation', () => {
12
+ expect(() => html`<div>Hello</div>`).toThrow();
13
+ });
14
+
15
+ it('throws with appropriate error message', () => {
16
+ expect(() => html`<div>Hello</div>`).toThrow(/vite-plugin/i);
17
+ });
18
+ });
19
+
20
+ describe('html.reactive', () => {
21
+ it('is a function', () => {
22
+ expect(typeof html.reactive).toBe('function');
23
+ });
24
+
25
+ it('throws when called without compilation', () => {
26
+ expect(() => html.reactive([] as unknown[], () => document.createDocumentFragment())).toThrow();
27
+ });
28
+
29
+ it('throws with appropriate error message', () => {
30
+ expect(() => html.reactive([] as unknown[], () => document.createDocumentFragment())).toThrow(/vite-plugin/i);
31
+ });
32
+ });
33
+ });