@esportsplus/template 0.16.0
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/.editorconfig +9 -0
- package/.gitattributes +2 -0
- package/.github/dependabot.yml +25 -0
- package/.github/workflows/bump.yml +9 -0
- package/.github/workflows/dependabot.yml +12 -0
- package/.github/workflows/publish.yml +16 -0
- package/README.md +385 -0
- package/build/attributes.d.ts +5 -0
- package/build/attributes.js +212 -0
- package/build/compiler/codegen.d.ts +21 -0
- package/build/compiler/codegen.js +303 -0
- package/build/compiler/constants.d.ts +16 -0
- package/build/compiler/constants.js +19 -0
- package/build/compiler/index.d.ts +14 -0
- package/build/compiler/index.js +61 -0
- package/build/compiler/parser.d.ts +19 -0
- package/build/compiler/parser.js +164 -0
- package/build/compiler/plugins/tsc.d.ts +3 -0
- package/build/compiler/plugins/tsc.js +4 -0
- package/build/compiler/plugins/vite.d.ts +13 -0
- package/build/compiler/plugins/vite.js +8 -0
- package/build/compiler/ts-analyzer.d.ts +4 -0
- package/build/compiler/ts-analyzer.js +63 -0
- package/build/compiler/ts-parser.d.ts +24 -0
- package/build/compiler/ts-parser.js +67 -0
- package/build/constants.d.ts +12 -0
- package/build/constants.js +25 -0
- package/build/event/index.d.ts +10 -0
- package/build/event/index.js +90 -0
- package/build/event/onconnect.d.ts +3 -0
- package/build/event/onconnect.js +15 -0
- package/build/event/onresize.d.ts +3 -0
- package/build/event/onresize.js +26 -0
- package/build/event/ontick.d.ts +6 -0
- package/build/event/ontick.js +41 -0
- package/build/html.d.ts +9 -0
- package/build/html.js +7 -0
- package/build/index.d.ts +8 -0
- package/build/index.js +12 -0
- package/build/render.d.ts +3 -0
- package/build/render.js +8 -0
- package/build/slot/array.d.ts +25 -0
- package/build/slot/array.js +189 -0
- package/build/slot/cleanup.d.ts +4 -0
- package/build/slot/cleanup.js +23 -0
- package/build/slot/effect.d.ts +12 -0
- package/build/slot/effect.js +85 -0
- package/build/slot/index.d.ts +7 -0
- package/build/slot/index.js +14 -0
- package/build/slot/render.d.ts +2 -0
- package/build/slot/render.js +44 -0
- package/build/svg.d.ts +5 -0
- package/build/svg.js +14 -0
- package/build/types.d.ts +23 -0
- package/build/types.js +1 -0
- package/build/utilities.d.ts +7 -0
- package/build/utilities.js +31 -0
- package/package.json +43 -0
- package/src/attributes.ts +313 -0
- package/src/compiler/codegen.ts +492 -0
- package/src/compiler/constants.ts +25 -0
- package/src/compiler/index.ts +87 -0
- package/src/compiler/parser.ts +242 -0
- package/src/compiler/plugins/tsc.ts +6 -0
- package/src/compiler/plugins/vite.ts +10 -0
- package/src/compiler/ts-analyzer.ts +89 -0
- package/src/compiler/ts-parser.ts +112 -0
- package/src/constants.ts +44 -0
- package/src/event/index.ts +130 -0
- package/src/event/onconnect.ts +22 -0
- package/src/event/onresize.ts +37 -0
- package/src/event/ontick.ts +59 -0
- package/src/html.ts +18 -0
- package/src/index.ts +19 -0
- package/src/llm.txt +403 -0
- package/src/render.ts +13 -0
- package/src/slot/array.ts +257 -0
- package/src/slot/cleanup.ts +37 -0
- package/src/slot/effect.ts +114 -0
- package/src/slot/index.ts +17 -0
- package/src/slot/render.ts +61 -0
- package/src/svg.ts +27 -0
- package/src/types.ts +40 -0
- package/src/utilities.ts +53 -0
- package/storage/compiler-architecture-2026-01-13.md +420 -0
- package/test/dist/test.js +1912 -0
- package/test/dist/test.js.map +1 -0
- package/test/index.ts +648 -0
- package/test/vite.config.ts +23 -0
- package/tsconfig.json +8 -0
package/test/index.ts
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensive compiler integration tests
|
|
3
|
+
* Tests both reactivity and template compilers working together
|
|
4
|
+
*/
|
|
5
|
+
import { html, reactive } from '@esportsplus/frontend';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Test 1: Primitive Signals in Templates
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
function testPrimitiveSignals() {
|
|
13
|
+
let count = reactive(0),
|
|
14
|
+
enabled = reactive(true),
|
|
15
|
+
message = reactive('Hello'),
|
|
16
|
+
price = reactive(19.99);
|
|
17
|
+
|
|
18
|
+
// Template with primitive expressions
|
|
19
|
+
let view = html`
|
|
20
|
+
<div class="counter">
|
|
21
|
+
<span class="count">${count}</span>
|
|
22
|
+
<span class="enabled">${enabled}</span>
|
|
23
|
+
<span class="message">${message}</span>
|
|
24
|
+
<span class="price">${price}</span>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
// Increment/decrement operations
|
|
29
|
+
count++;
|
|
30
|
+
count--;
|
|
31
|
+
count += 5;
|
|
32
|
+
count -= 2;
|
|
33
|
+
count *= 2;
|
|
34
|
+
|
|
35
|
+
// Boolean toggle
|
|
36
|
+
enabled = !enabled;
|
|
37
|
+
|
|
38
|
+
// String operations
|
|
39
|
+
message = message + ' World';
|
|
40
|
+
|
|
41
|
+
return { count, enabled, message, price, view };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Test 2: Computed Values in Templates
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
function testComputedValues() {
|
|
50
|
+
let firstName = reactive('John'),
|
|
51
|
+
lastName = reactive('Doe'),
|
|
52
|
+
items = reactive([1, 2, 3, 4, 5] as number[]);
|
|
53
|
+
|
|
54
|
+
// Computed values
|
|
55
|
+
let fullName = reactive(() => `${firstName} ${lastName}`),
|
|
56
|
+
itemCount = reactive(() => items.length),
|
|
57
|
+
doubled = reactive(() => items.map(x => x * 2)),
|
|
58
|
+
total = reactive(() => items.reduce((a, b) => a + b, 0));
|
|
59
|
+
|
|
60
|
+
let view = html`
|
|
61
|
+
<div class="computed">
|
|
62
|
+
<h1>${fullName}</h1>
|
|
63
|
+
<p>Items: ${itemCount}</p>
|
|
64
|
+
<p>Total: ${total}</p>
|
|
65
|
+
<ul>
|
|
66
|
+
${() => doubled.map(n => html`<li>${n}</li>`)}
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
// Trigger reactivity
|
|
72
|
+
firstName = 'Jane';
|
|
73
|
+
items.push(6);
|
|
74
|
+
|
|
75
|
+
return { doubled, firstName, fullName, itemCount, items, lastName, total, view };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Test 3: Reactive Arrays in Templates
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
function testReactiveArrays() {
|
|
84
|
+
let numbers = reactive([1, 2, 3] as number[]),
|
|
85
|
+
strings = reactive(['a', 'b', 'c'] as string[]),
|
|
86
|
+
mixed = reactive([1, 'two', 3, 'four'] as (number | string)[]);
|
|
87
|
+
|
|
88
|
+
// Array with reactive rendering
|
|
89
|
+
let view = html`
|
|
90
|
+
<div class="arrays">
|
|
91
|
+
<section>
|
|
92
|
+
<h2>Numbers</h2>
|
|
93
|
+
${html.reactive(numbers, (n) => html`<span class="num">${n}</span>`)}
|
|
94
|
+
</section>
|
|
95
|
+
<section>
|
|
96
|
+
<h2>Strings</h2>
|
|
97
|
+
${html.reactive(strings, (s) => html`<span class="str">${s}</span>`)}
|
|
98
|
+
</section>
|
|
99
|
+
<section>
|
|
100
|
+
<h2>Mixed</h2>
|
|
101
|
+
${html.reactive(mixed, (m) => html`<span class="mix">${m}</span>`)}
|
|
102
|
+
</section>
|
|
103
|
+
</div>
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
// Array mutations
|
|
107
|
+
numbers.push(4);
|
|
108
|
+
numbers.unshift(0);
|
|
109
|
+
numbers[2] = 99;
|
|
110
|
+
|
|
111
|
+
strings.pop();
|
|
112
|
+
strings.shift();
|
|
113
|
+
strings.splice(0, 0, 'inserted');
|
|
114
|
+
|
|
115
|
+
// Length access (reactive)
|
|
116
|
+
let numLen = numbers.length,
|
|
117
|
+
strLen = strings.length;
|
|
118
|
+
|
|
119
|
+
return { mixed, numLen, numbers, strLen, strings, view };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Test 4: Reactive Objects in Templates
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
function testReactiveObjects() {
|
|
128
|
+
// Simple reactive object
|
|
129
|
+
let user = reactive({
|
|
130
|
+
age: 25,
|
|
131
|
+
email: 'john@example.com',
|
|
132
|
+
name: 'John'
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Reactive object with computed
|
|
136
|
+
let product = reactive({
|
|
137
|
+
discount: 0.1,
|
|
138
|
+
finalPrice: () => product.price * (1 - product.discount),
|
|
139
|
+
price: 100
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Nested data in reactive object
|
|
143
|
+
let config = reactive({
|
|
144
|
+
debug: false,
|
|
145
|
+
features: {
|
|
146
|
+
darkMode: true,
|
|
147
|
+
notifications: false
|
|
148
|
+
},
|
|
149
|
+
theme: 'light'
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
let view = html`
|
|
153
|
+
<div class="objects">
|
|
154
|
+
<section class="user">
|
|
155
|
+
<h2>${user.name}</h2>
|
|
156
|
+
<p>Age: ${user.age}</p>
|
|
157
|
+
<p>Email: ${user.email}</p>
|
|
158
|
+
</section>
|
|
159
|
+
<section class="product">
|
|
160
|
+
<p>Price: $${product.price}</p>
|
|
161
|
+
<p>Discount: ${() => product.discount * 100}%</p>
|
|
162
|
+
<p>Final: $${product.finalPrice}</p>
|
|
163
|
+
</section>
|
|
164
|
+
<section class="config">
|
|
165
|
+
<p>Theme: ${config.theme}</p>
|
|
166
|
+
<p>Debug: ${config.debug}</p>
|
|
167
|
+
</section>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
// Object mutations
|
|
172
|
+
user.name = 'Jane';
|
|
173
|
+
user.age++;
|
|
174
|
+
|
|
175
|
+
product.price = 150;
|
|
176
|
+
product.discount = 0.2;
|
|
177
|
+
|
|
178
|
+
config.theme = 'dark';
|
|
179
|
+
config.debug = true;
|
|
180
|
+
|
|
181
|
+
return { config, product, user, view };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Test 5: Complex Nested Reactivity
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
function testComplexNested() {
|
|
190
|
+
let state = reactive({
|
|
191
|
+
counts: reactive([0, 0, 0] as number[]),
|
|
192
|
+
currentIndex: 0,
|
|
193
|
+
increment: () => {
|
|
194
|
+
state.counts[state.currentIndex]++;
|
|
195
|
+
},
|
|
196
|
+
items: reactive([
|
|
197
|
+
{ id: 1, name: 'Item 1', selected: false },
|
|
198
|
+
{ id: 2, name: 'Item 2', selected: true },
|
|
199
|
+
{ id: 3, name: 'Item 3', selected: false }
|
|
200
|
+
] as { id: number; name: string; selected: boolean }[]),
|
|
201
|
+
selectedCount: () => state.items.filter(i => i.selected).length,
|
|
202
|
+
total: () => state.counts.reduce((a, b) => a + b, 0)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
let view = html`
|
|
206
|
+
<div class="complex">
|
|
207
|
+
<header>
|
|
208
|
+
<span>Total: ${state.total}</span>
|
|
209
|
+
<span>Selected: ${state.selectedCount}</span>
|
|
210
|
+
</header>
|
|
211
|
+
<main>
|
|
212
|
+
${html.reactive(state.items, (item) => html`
|
|
213
|
+
<div class="item ${() => item.selected ? 'selected' : ''}">
|
|
214
|
+
<span>${item.name}</span>
|
|
215
|
+
<input type="checkbox" checked=${() => item.selected} />
|
|
216
|
+
</div>
|
|
217
|
+
`)}
|
|
218
|
+
</main>
|
|
219
|
+
<footer>
|
|
220
|
+
${html.reactive(state.counts, (count, i) => html`
|
|
221
|
+
<button onclick=${() => { state.currentIndex = i; state.increment(); }}>
|
|
222
|
+
Count ${i}: ${count}
|
|
223
|
+
</button>
|
|
224
|
+
`)}
|
|
225
|
+
</footer>
|
|
226
|
+
</div>
|
|
227
|
+
`;
|
|
228
|
+
|
|
229
|
+
// Trigger updates
|
|
230
|
+
state.items[0].selected = true;
|
|
231
|
+
state.counts[0] = 5;
|
|
232
|
+
state.currentIndex = 1;
|
|
233
|
+
|
|
234
|
+
return { state, view };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Test 6: Conditional Rendering with Reactivity
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
function testConditionalRendering() {
|
|
243
|
+
let showDetails = reactive(false),
|
|
244
|
+
status = reactive<'loading' | 'error' | 'success'>('loading'),
|
|
245
|
+
user = reactive<{ name: string } | null>(null);
|
|
246
|
+
|
|
247
|
+
let view = html`
|
|
248
|
+
<div class="conditional">
|
|
249
|
+
${() => showDetails ? html`<details>Expanded content</details>` : html`<summary>Click to expand</summary>`}
|
|
250
|
+
|
|
251
|
+
${() => {
|
|
252
|
+
if (status === 'loading') {
|
|
253
|
+
return html`<div class="loading">Loading...</div>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (status === 'error') {
|
|
257
|
+
return html`<div class="error">Error occurred</div>`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return html`<div class="success">Success!</div>`;
|
|
261
|
+
}}
|
|
262
|
+
|
|
263
|
+
${() => user ? html`<span>Welcome, ${user.name}</span>` : html`<span>Please log in</span>`}
|
|
264
|
+
</div>
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
// Toggle states
|
|
268
|
+
showDetails = true;
|
|
269
|
+
status = 'success';
|
|
270
|
+
user = { name: 'Alice' };
|
|
271
|
+
|
|
272
|
+
return { showDetails, status, user, view };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Test 7: Event Handlers with Reactive State
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
function testEventHandlers() {
|
|
281
|
+
let clicks = reactive(0),
|
|
282
|
+
inputValue = reactive(''),
|
|
283
|
+
items = reactive([] as string[]);
|
|
284
|
+
|
|
285
|
+
let view = html`
|
|
286
|
+
<div class="events">
|
|
287
|
+
<button onclick=${() => clicks++}>
|
|
288
|
+
Clicked ${clicks} times
|
|
289
|
+
</button>
|
|
290
|
+
|
|
291
|
+
<input
|
|
292
|
+
type="text"
|
|
293
|
+
value=${inputValue}
|
|
294
|
+
oninput=${(e: Event) => { inputValue = (e.target as HTMLInputElement).value; }}
|
|
295
|
+
/>
|
|
296
|
+
|
|
297
|
+
<button onclick=${() => {
|
|
298
|
+
if (inputValue) {
|
|
299
|
+
items.push(inputValue);
|
|
300
|
+
inputValue = '';
|
|
301
|
+
}
|
|
302
|
+
}}>
|
|
303
|
+
Add Item
|
|
304
|
+
</button>
|
|
305
|
+
|
|
306
|
+
<ul>
|
|
307
|
+
${html.reactive(items, (item, index) => html`
|
|
308
|
+
<li>
|
|
309
|
+
${item}
|
|
310
|
+
<button onclick=${() => items.splice(index, 1)}>Remove</button>
|
|
311
|
+
</li>
|
|
312
|
+
`)}
|
|
313
|
+
</ul>
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
// Simulate interactions
|
|
318
|
+
clicks++;
|
|
319
|
+
clicks++;
|
|
320
|
+
inputValue = 'Test item';
|
|
321
|
+
items.push('First');
|
|
322
|
+
items.push('Second');
|
|
323
|
+
|
|
324
|
+
return { clicks, inputValue, items, view };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Test 8: Attribute Bindings with Reactivity
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
function testAttributeBindings() {
|
|
333
|
+
let isDisabled = reactive(false),
|
|
334
|
+
classes = reactive('primary'),
|
|
335
|
+
styles = reactive('color: red'),
|
|
336
|
+
href = reactive('https://example.com'),
|
|
337
|
+
dataValue = reactive(42);
|
|
338
|
+
|
|
339
|
+
let view = html`
|
|
340
|
+
<div class="attributes">
|
|
341
|
+
<button
|
|
342
|
+
class=${classes}
|
|
343
|
+
disabled=${isDisabled}
|
|
344
|
+
data-value=${dataValue}
|
|
345
|
+
style=${styles}
|
|
346
|
+
>
|
|
347
|
+
Dynamic Button
|
|
348
|
+
</button>
|
|
349
|
+
|
|
350
|
+
<a href=${href} target="_blank">Dynamic Link</a>
|
|
351
|
+
|
|
352
|
+
<input
|
|
353
|
+
type="text"
|
|
354
|
+
class=${() => isDisabled ? 'disabled-input' : 'active-input'}
|
|
355
|
+
placeholder=${() => `Value: ${dataValue}`}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
358
|
+
`;
|
|
359
|
+
|
|
360
|
+
// Update attributes
|
|
361
|
+
isDisabled = true;
|
|
362
|
+
classes = 'secondary active';
|
|
363
|
+
styles = 'color: blue; font-weight: bold';
|
|
364
|
+
href = 'https://updated.com';
|
|
365
|
+
dataValue = 100;
|
|
366
|
+
|
|
367
|
+
return { classes, dataValue, href, isDisabled, styles, view };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// Test 9: Mixed Reactive Types in Single Template
|
|
373
|
+
// ============================================================================
|
|
374
|
+
|
|
375
|
+
function testMixedReactiveTypes() {
|
|
376
|
+
// All types in one component
|
|
377
|
+
let primitive = reactive(0),
|
|
378
|
+
computed = reactive(() => primitive * 2),
|
|
379
|
+
array = reactive([1, 2, 3] as number[]),
|
|
380
|
+
object = reactive({
|
|
381
|
+
count: 0,
|
|
382
|
+
double: () => object.count * 2,
|
|
383
|
+
name: 'Test'
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
let view = html`
|
|
387
|
+
<div class="mixed">
|
|
388
|
+
<section>
|
|
389
|
+
<h3>Primitive: ${primitive}</h3>
|
|
390
|
+
<h3>Computed: ${computed}</h3>
|
|
391
|
+
</section>
|
|
392
|
+
|
|
393
|
+
<section>
|
|
394
|
+
<h3>Object Name: ${object.name}</h3>
|
|
395
|
+
<h3>Object Count: ${object.count}</h3>
|
|
396
|
+
<h3>Object Double: ${object.double}</h3>
|
|
397
|
+
</section>
|
|
398
|
+
|
|
399
|
+
<section>
|
|
400
|
+
<h3>Array Length: ${() => array.length}</h3>
|
|
401
|
+
${html.reactive(array, (n) => html`<span>${n}</span>`)}
|
|
402
|
+
</section>
|
|
403
|
+
|
|
404
|
+
<section>
|
|
405
|
+
<button onclick=${() => {
|
|
406
|
+
primitive++;
|
|
407
|
+
object.count++;
|
|
408
|
+
array.push(array.length + 1);
|
|
409
|
+
}}>
|
|
410
|
+
Increment All
|
|
411
|
+
</button>
|
|
412
|
+
</section>
|
|
413
|
+
</div>
|
|
414
|
+
`;
|
|
415
|
+
|
|
416
|
+
// Trigger all types
|
|
417
|
+
primitive = 5;
|
|
418
|
+
object.name = 'Updated';
|
|
419
|
+
object.count = 10;
|
|
420
|
+
array.push(4, 5, 6);
|
|
421
|
+
|
|
422
|
+
return { array, computed, object, primitive, view };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
// ============================================================================
|
|
427
|
+
// Test 10: Typed Reactive with Generics
|
|
428
|
+
// ============================================================================
|
|
429
|
+
|
|
430
|
+
interface Todo {
|
|
431
|
+
completed: boolean;
|
|
432
|
+
id: number;
|
|
433
|
+
text: string;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
interface AppState {
|
|
437
|
+
addTodo: (text: string) => void;
|
|
438
|
+
completedCount: () => number;
|
|
439
|
+
filter: 'all' | 'active' | 'completed';
|
|
440
|
+
filteredTodos: () => Todo[];
|
|
441
|
+
nextId: number;
|
|
442
|
+
todos: Todo[];
|
|
443
|
+
toggleTodo: (id: number) => void;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function testTypedReactive() {
|
|
447
|
+
let state = reactive<AppState>({
|
|
448
|
+
addTodo: (text: string) => {
|
|
449
|
+
state.todos.push({
|
|
450
|
+
completed: false,
|
|
451
|
+
id: state.nextId++,
|
|
452
|
+
text
|
|
453
|
+
});
|
|
454
|
+
},
|
|
455
|
+
completedCount: () => state.todos.filter(t => t.completed).length,
|
|
456
|
+
filter: 'all',
|
|
457
|
+
filteredTodos: () => {
|
|
458
|
+
if (state.filter === 'active') {
|
|
459
|
+
return state.todos.filter(t => !t.completed);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (state.filter === 'completed') {
|
|
463
|
+
return state.todos.filter(t => t.completed);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return state.todos;
|
|
467
|
+
},
|
|
468
|
+
nextId: 1,
|
|
469
|
+
todos: [],
|
|
470
|
+
toggleTodo: (id: number) => {
|
|
471
|
+
let todo = state.todos.find(t => t.id === id);
|
|
472
|
+
|
|
473
|
+
if (todo) {
|
|
474
|
+
todo.completed = !todo.completed;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
let view = html`
|
|
480
|
+
<div class="todo-app">
|
|
481
|
+
<header>
|
|
482
|
+
<h1>Todos (${state.completedCount} completed)</h1>
|
|
483
|
+
<nav>
|
|
484
|
+
<button
|
|
485
|
+
class=${() => state.filter === 'all' ? 'active' : ''}
|
|
486
|
+
onclick=${() => { state.filter = 'all'; }}
|
|
487
|
+
>All</button>
|
|
488
|
+
<button
|
|
489
|
+
class=${() => state.filter === 'active' ? 'active' : ''}
|
|
490
|
+
onclick=${() => { state.filter = 'active'; }}
|
|
491
|
+
>Active</button>
|
|
492
|
+
<button
|
|
493
|
+
class=${() => state.filter === 'completed' ? 'active' : ''}
|
|
494
|
+
onclick=${() => { state.filter = 'completed'; }}
|
|
495
|
+
>Completed</button>
|
|
496
|
+
</nav>
|
|
497
|
+
</header>
|
|
498
|
+
|
|
499
|
+
<main>
|
|
500
|
+
${() => state.filteredTodos().map(todo => html`
|
|
501
|
+
<div class="todo ${todo.completed ? 'completed' : ''}">
|
|
502
|
+
<input
|
|
503
|
+
type="checkbox"
|
|
504
|
+
checked=${todo.completed}
|
|
505
|
+
onchange=${() => state.toggleTodo(todo.id)}
|
|
506
|
+
/>
|
|
507
|
+
<span>${todo.text}</span>
|
|
508
|
+
</div>
|
|
509
|
+
`)}
|
|
510
|
+
</main>
|
|
511
|
+
</div>
|
|
512
|
+
`;
|
|
513
|
+
|
|
514
|
+
// Add some todos
|
|
515
|
+
state.addTodo('Learn TypeScript');
|
|
516
|
+
state.addTodo('Build app');
|
|
517
|
+
state.addTodo('Write tests');
|
|
518
|
+
state.toggleTodo(1);
|
|
519
|
+
|
|
520
|
+
return { state, view };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// Test 11: Deeply Nested Templates with Reactivity
|
|
526
|
+
// ============================================================================
|
|
527
|
+
|
|
528
|
+
function testDeeplyNested() {
|
|
529
|
+
let level1 = reactive({
|
|
530
|
+
items: reactive([
|
|
531
|
+
{
|
|
532
|
+
children: reactive([
|
|
533
|
+
{ name: 'Leaf 1', value: reactive(1) },
|
|
534
|
+
{ name: 'Leaf 2', value: reactive(2) }
|
|
535
|
+
] as { name: string; value: number }[]),
|
|
536
|
+
name: 'Child 1'
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
children: reactive([
|
|
540
|
+
{ name: 'Leaf 3', value: reactive(3) }
|
|
541
|
+
] as { name: string; value: number }[]),
|
|
542
|
+
name: 'Child 2'
|
|
543
|
+
}
|
|
544
|
+
] as { children: { name: string; value: number }[]; name: string }[]),
|
|
545
|
+
name: 'Root'
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
let view = html`
|
|
549
|
+
<div class="tree">
|
|
550
|
+
<h1>${level1.name}</h1>
|
|
551
|
+
${html.reactive(level1.items, (item) => html`
|
|
552
|
+
<div class="branch">
|
|
553
|
+
<h2>${item.name}</h2>
|
|
554
|
+
${html.reactive(item.children, (child) => html`
|
|
555
|
+
<div class="leaf">
|
|
556
|
+
<span>${child.name}: ${child.value}</span>
|
|
557
|
+
<button onclick=${() => child.value++}>+</button>
|
|
558
|
+
</div>
|
|
559
|
+
`)}
|
|
560
|
+
</div>
|
|
561
|
+
`)}
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
|
|
565
|
+
// Mutate nested values
|
|
566
|
+
level1.items[0].children[0].value++;
|
|
567
|
+
level1.items.push({
|
|
568
|
+
children: reactive([{ name: 'Leaf 4', value: reactive(4) }]),
|
|
569
|
+
name: 'Child 3'
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
return { level1, view };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Test 12: Static vs Dynamic Content
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
580
|
+
function testStaticVsDynamic() {
|
|
581
|
+
let dynamicText = reactive('Dynamic'),
|
|
582
|
+
dynamicNum = reactive(42),
|
|
583
|
+
dynamicBool = reactive(true);
|
|
584
|
+
|
|
585
|
+
// Mix of static and dynamic content
|
|
586
|
+
let view = html`
|
|
587
|
+
<div class="static-dynamic">
|
|
588
|
+
<p>Static text here</p>
|
|
589
|
+
<p>${dynamicText} text here</p>
|
|
590
|
+
|
|
591
|
+
<span>Static number: 100</span>
|
|
592
|
+
<span>Dynamic number: ${dynamicNum}</span>
|
|
593
|
+
|
|
594
|
+
<div class="static-class">Static class</div>
|
|
595
|
+
<div class="${() => dynamicBool ? 'dynamic-true' : 'dynamic-false'}">Dynamic class</div>
|
|
596
|
+
|
|
597
|
+
<a href="https://static.com">Static link</a>
|
|
598
|
+
<a href=${() => dynamicBool ? 'https://true.com' : 'https://false.com'}>Dynamic link</a>
|
|
599
|
+
|
|
600
|
+
<input type="text" value="static" />
|
|
601
|
+
<input type="text" value=${dynamicText} />
|
|
602
|
+
</div>
|
|
603
|
+
`;
|
|
604
|
+
|
|
605
|
+
dynamicText = 'Updated';
|
|
606
|
+
dynamicNum = 100;
|
|
607
|
+
dynamicBool = false;
|
|
608
|
+
|
|
609
|
+
return { dynamicBool, dynamicNum, dynamicText, view };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// Run All Tests
|
|
615
|
+
// ============================================================================
|
|
616
|
+
|
|
617
|
+
export const tests = {
|
|
618
|
+
testAttributeBindings,
|
|
619
|
+
testComplexNested,
|
|
620
|
+
testComputedValues,
|
|
621
|
+
testConditionalRendering,
|
|
622
|
+
testDeeplyNested,
|
|
623
|
+
testEventHandlers,
|
|
624
|
+
testMixedReactiveTypes,
|
|
625
|
+
testPrimitiveSignals,
|
|
626
|
+
testReactiveArrays,
|
|
627
|
+
testReactiveObjects,
|
|
628
|
+
testStaticVsDynamic,
|
|
629
|
+
testTypedReactive
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Execute all tests to verify compilation
|
|
633
|
+
export const results = Object.entries(tests).map(([name, fn]) => {
|
|
634
|
+
try {
|
|
635
|
+
let result = fn();
|
|
636
|
+
|
|
637
|
+
console.log(`✓ ${name} passed`);
|
|
638
|
+
|
|
639
|
+
return { name, result, status: 'passed' };
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
console.error(`✗ ${name} failed:`, error);
|
|
643
|
+
|
|
644
|
+
return { error, name, status: 'failed' };
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
console.log(`\nTests completed: ${results.filter(r => r.status === 'passed').length}/${results.length} passed`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import compiler from '@esportsplus/frontend/compiler/vite';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
build: {
|
|
8
|
+
lib: {
|
|
9
|
+
entry: path.resolve(__dirname, 'index.ts'),
|
|
10
|
+
fileName: 'test',
|
|
11
|
+
formats: ['es']
|
|
12
|
+
},
|
|
13
|
+
minify: false,
|
|
14
|
+
outDir: path.resolve(__dirname, 'dist'),
|
|
15
|
+
rollupOptions: {
|
|
16
|
+
external: []
|
|
17
|
+
},
|
|
18
|
+
sourcemap: true
|
|
19
|
+
},
|
|
20
|
+
plugins: [
|
|
21
|
+
compiler({ root: path.resolve(__dirname, '..') })
|
|
22
|
+
]
|
|
23
|
+
});
|