@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,292 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ts } from '@esportsplus/typescript';
3
+ import { generateCode, rewriteExpression } from '../../src/compiler/codegen';
4
+ import { findHtmlTemplates } from '../../src/compiler/ts-parser';
5
+ import { NAMESPACE } from '../../src/compiler/constants';
6
+
7
+
8
+ function codegen(source: string) {
9
+ let sourceFile = ts.createSourceFile('test.ts', source, ts.ScriptTarget.Latest, true),
10
+ templates = findHtmlTemplates(sourceFile);
11
+
12
+ return { result: generateCode(templates, sourceFile), sourceFile, templates };
13
+ }
14
+
15
+
16
+ describe('compiler/codegen', () => {
17
+ describe('generateCode - static templates', () => {
18
+ it('returns empty result for no templates', () => {
19
+ let { result } = codegen('let x = 1;');
20
+
21
+ expect(result.prepend).toHaveLength(0);
22
+ expect(result.replacements).toHaveLength(0);
23
+ expect(result.templates.size).toBe(0);
24
+ });
25
+
26
+ it('generates template factory for static template', () => {
27
+ let { result } = codegen(`let x = html\`<div>hello</div>\`;`);
28
+
29
+ expect(result.templates.size).toBe(1);
30
+ expect(result.prepend).toHaveLength(1);
31
+ expect(result.prepend[0]).toContain(`${NAMESPACE}.template(\`<div>hello</div>\`)`);
32
+ expect(result.replacements).toHaveLength(1);
33
+
34
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
35
+
36
+ expect(code).toContain('()');
37
+ });
38
+
39
+ it('generates walker for static template with no slots', () => {
40
+ let { result } = codegen(`let x = html\`<div>hello</div>\`;`);
41
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
42
+
43
+ // Static template without slots just calls the template factory
44
+ expect(code).toMatch(/^[a-zA-Z_$][\w$]*\(\)$/);
45
+ });
46
+ });
47
+
48
+ describe('generateCode - node slots', () => {
49
+ it('generates slot binding for node expression', () => {
50
+ let { result } = codegen(`let x = html\`<div>\${value}</div>\`;`);
51
+
52
+ expect(result.replacements).toHaveLength(1);
53
+
54
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
55
+
56
+ // Should contain slot() call for unknown type expression
57
+ expect(code).toContain(`${NAMESPACE}.slot(`);
58
+ expect(code).toContain('value');
59
+ expect(code).toContain('return');
60
+ });
61
+
62
+ it('generates EffectSlot for arrow function expression', () => {
63
+ let { result } = codegen(`let x = html\`<div>\${() => count}</div>\`;`);
64
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
65
+
66
+ expect(code).toContain(`${NAMESPACE}.EffectSlot`);
67
+ });
68
+
69
+ it('generates static text binding for string literal', () => {
70
+ let { result } = codegen(`let x = html\`<div>\${"hello"}</div>\`;`);
71
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
72
+
73
+ expect(code).toContain(`${NAMESPACE}.text(`);
74
+ expect(code).toContain('"hello"');
75
+ });
76
+
77
+ it('generates insertBefore for nested html template', () => {
78
+ let { result } = codegen(`let x = html\`<div>\${html\`<span>nested</span>\`}</div>\`;`);
79
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
80
+
81
+ expect(code).toContain('insertBefore');
82
+ });
83
+
84
+ it('generates multiple node bindings', () => {
85
+ let { result } = codegen(`let x = html\`<div>\${a}</div><span>\${b}</span>\`;`);
86
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
87
+
88
+ expect(code).toContain('a');
89
+ expect(code).toContain('b');
90
+ });
91
+ });
92
+
93
+ describe('generateCode - attribute slots', () => {
94
+ it('generates setProperty for attribute binding', () => {
95
+ let { result } = codegen(`let x = html\`<div id=\${value}>text</div>\`;`);
96
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
97
+
98
+ expect(code).toContain(`${NAMESPACE}.setProperty(`);
99
+ expect(code).toContain("'id'");
100
+ expect(code).toContain('value');
101
+ });
102
+
103
+ it('generates setList for class binding', () => {
104
+ let { result } = codegen(`let x = html\`<div class=\${cls}>text</div>\`;`);
105
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
106
+
107
+ expect(code).toContain(`${NAMESPACE}.setList(`);
108
+ expect(code).toContain("'class'");
109
+ });
110
+
111
+ it('generates setList for style binding', () => {
112
+ let { result } = codegen(`let x = html\`<div style=\${sty}>text</div>\`;`);
113
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
114
+
115
+ expect(code).toContain(`${NAMESPACE}.setList(`);
116
+ expect(code).toContain("'style'");
117
+ });
118
+ });
119
+
120
+ describe('generateCode - event attributes', () => {
121
+ it('generates delegate for standard event', () => {
122
+ let { result } = codegen(`let x = html\`<div onclick=\${handler}>text</div>\`;`);
123
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
124
+
125
+ expect(code).toContain(`${NAMESPACE}.delegate(`);
126
+ expect(code).toContain("'click'");
127
+ });
128
+
129
+ it('generates direct attach for focus event', () => {
130
+ let { result } = codegen(`let x = html\`<div onfocus=\${handler}>text</div>\`;`);
131
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
132
+
133
+ expect(code).toContain(`${NAMESPACE}.on(`);
134
+ expect(code).toContain("'focus'");
135
+ });
136
+
137
+ it('generates lifecycle call for onconnect', () => {
138
+ let { result } = codegen(`let x = html\`<div onconnect=\${handler}>text</div>\`;`);
139
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
140
+
141
+ expect(code).toContain(`${NAMESPACE}.onconnect(`);
142
+ });
143
+
144
+ it('generates lifecycle call for onresize', () => {
145
+ let { result } = codegen(`let x = html\`<div onresize=\${handler}>text</div>\`;`);
146
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
147
+
148
+ expect(code).toContain(`${NAMESPACE}.onresize(`);
149
+ });
150
+ });
151
+
152
+ describe('generateCode - nested templates', () => {
153
+ it('generates nested template with own factory', () => {
154
+ let { result } = codegen(`let x = html\`<div>\${html\`<span>inner</span>\`}</div>\`;`);
155
+
156
+ // Outer template + inner template
157
+ expect(result.templates.size).toBe(2);
158
+ expect(result.prepend).toHaveLength(2);
159
+ });
160
+
161
+ it('rewrites nested html`` in expressions', () => {
162
+ let { result } = codegen(`let x = html\`<div>\${condition ? html\`<span>a</span>\` : html\`<em>b</em>\`}</div>\`;`);
163
+
164
+ // Outer + 2 inner templates
165
+ expect(result.templates.size).toBe(3);
166
+ expect(result.prepend).toHaveLength(3);
167
+ });
168
+ });
169
+
170
+ describe('generateCode - html.reactive()', () => {
171
+ it('generates ArraySlot for reactive call in node slot', () => {
172
+ let { result } = codegen(`let x = html\`<div>\${html.reactive(items, (item) => html\`<span>\${item}</span>\`)}</div>\`;`);
173
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
174
+
175
+ expect(code).toContain(`${NAMESPACE}.ArraySlot`);
176
+ });
177
+ });
178
+
179
+ describe('generateCode - mixed slots', () => {
180
+ it('handles attribute + node slots on same element', () => {
181
+ let { result } = codegen(`let x = html\`<div class=\${cls}>\${content}</div>\`;`);
182
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
183
+
184
+ expect(code).toContain(`${NAMESPACE}.setList(`);
185
+ expect(code).toContain(`${NAMESPACE}.slot(`);
186
+ });
187
+
188
+ it('handles multiple attribute slots on same element', () => {
189
+ let { result } = codegen(`let x = html\`<div id=\${id} class=\${cls}>text</div>\`;`);
190
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
191
+
192
+ expect(code).toContain(`${NAMESPACE}.setProperty(`);
193
+ expect(code).toContain(`${NAMESPACE}.setList(`);
194
+ });
195
+ });
196
+
197
+ describe('generateCode - template deduplication', () => {
198
+ it('deduplicates identical template literals', () => {
199
+ let { result } = codegen(`let a = html\`<div>same</div>\`; let b = html\`<div>same</div>\`;`);
200
+
201
+ // Same HTML should produce only one template factory
202
+ expect(result.templates.size).toBe(1);
203
+ expect(result.prepend).toHaveLength(1);
204
+ expect(result.replacements).toHaveLength(2);
205
+ });
206
+
207
+ it('creates separate templates for different HTML', () => {
208
+ let { result } = codegen(`let a = html\`<div>one</div>\`; let b = html\`<span>two</span>\`;`);
209
+
210
+ expect(result.templates.size).toBe(2);
211
+ expect(result.prepend).toHaveLength(2);
212
+ });
213
+ });
214
+
215
+ describe('generateCode - expression type detection', () => {
216
+ it('generates EffectSlot for function expression node', () => {
217
+ let { result } = codegen(`let x = html\`<div>\${function() { return 1; }}</div>\`;`);
218
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
219
+
220
+ expect(code).toContain(`${NAMESPACE}.EffectSlot`);
221
+ });
222
+
223
+ it('generates slot() for unknown identifier', () => {
224
+ let { result } = codegen(`let x = html\`<div>\${someVar}</div>\`;`);
225
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
226
+
227
+ expect(code).toContain(`${NAMESPACE}.slot(`);
228
+ });
229
+
230
+ it('generates text() for numeric literal', () => {
231
+ let { result } = codegen(`let x = html\`<div>\${42}</div>\`;`);
232
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
233
+
234
+ expect(code).toContain(`${NAMESPACE}.text(`);
235
+ });
236
+ });
237
+
238
+ describe('rewriteExpression', () => {
239
+ it('rewrites nested html template in expression', () => {
240
+ let source = `let x = html\`<div>\${html\`<span>inner</span>\`}</div>\`;`,
241
+ sourceFile = ts.createSourceFile('test.ts', source, ts.ScriptTarget.Latest, true),
242
+ templates = findHtmlTemplates(sourceFile);
243
+
244
+ let ctx = {
245
+ sourceFile,
246
+ templates: new Map<string, string>()
247
+ };
248
+
249
+ let expr = templates[0].expressions[0],
250
+ rewritten = rewriteExpression(ctx, expr);
251
+
252
+ // Nested html`` should be rewritten to template factory call
253
+ expect(rewritten).not.toContain('html`');
254
+ });
255
+
256
+ it('prints plain expression as-is', () => {
257
+ let source = `let x = html\`<div>\${value}</div>\`;`,
258
+ sourceFile = ts.createSourceFile('test.ts', source, ts.ScriptTarget.Latest, true),
259
+ templates = findHtmlTemplates(sourceFile);
260
+
261
+ let ctx = {
262
+ sourceFile,
263
+ templates: new Map<string, string>()
264
+ };
265
+
266
+ let expr = templates[0].expressions[0],
267
+ rewritten = rewriteExpression(ctx, expr);
268
+
269
+ expect(rewritten).toBe('value');
270
+ });
271
+ });
272
+
273
+ describe('generateCode - arrow function body optimization', () => {
274
+ it('generates template ID directly for parameterless arrow with static body', () => {
275
+ let { result } = codegen(`let fn = () => html\`<div>static</div>\`;`);
276
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
277
+
278
+ // Should be just the template ID (no () call, no IIFE)
279
+ expect(code).not.toContain('()');
280
+ expect(code).not.toContain('return');
281
+ });
282
+
283
+ it('generates IIFE for arrow with slots', () => {
284
+ let { result } = codegen(`let fn = () => html\`<div>\${value}</div>\`;`);
285
+ let code = result.replacements[0].generate(ts.createSourceFile('', '', ts.ScriptTarget.Latest));
286
+
287
+ // Arrow body with slots uses block syntax
288
+ expect(code).toContain('{');
289
+ expect(code).toContain('return');
290
+ });
291
+ });
292
+ });
@@ -0,0 +1,252 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ts } from '@esportsplus/typescript';
3
+ import { ast } from '@esportsplus/typescript/compiler';
4
+ import { generateCode, rewriteExpression } from '../../src/compiler/codegen';
5
+ import { NAMESPACE } from '../../src/compiler/constants';
6
+ import { findHtmlTemplates, findReactiveCalls } from '../../src/compiler/ts-parser';
7
+
8
+
9
+ type TransformResult = {
10
+ imports: string[];
11
+ output: string;
12
+ prepend: string[];
13
+ replacements: { code: string; end: number; start: number }[];
14
+ };
15
+
16
+
17
+ function pipeline(source: string): TransformResult {
18
+ let callRanges: { end: number; start: number }[] = [],
19
+ callTemplates = new Map<string, string>(),
20
+ imports: string[] = [],
21
+ prepend: string[] = [],
22
+ replacements: { code: string; end: number; start: number }[] = [],
23
+ sourceFile = ts.createSourceFile('test.ts', source, ts.ScriptTarget.Latest, true),
24
+ templates = findHtmlTemplates(sourceFile);
25
+
26
+ let ranges: { end: number; start: number }[] = [];
27
+
28
+ for (let i = 0, n = templates.length; i < n; i++) {
29
+ ranges.push({
30
+ end: templates[i].end,
31
+ start: templates[i].start
32
+ });
33
+ }
34
+
35
+ let calls = findReactiveCalls(sourceFile);
36
+
37
+ for (let i = 0, n = calls.length; i < n; i++) {
38
+ let call = calls[i];
39
+
40
+ if (ast.inRange(ranges, call.start, call.end)) {
41
+ continue;
42
+ }
43
+
44
+ callRanges.push({
45
+ end: call.callbackArg.end,
46
+ start: call.callbackArg.getStart(sourceFile)
47
+ });
48
+
49
+ let rewrittenCallback = rewriteExpression({
50
+ sourceFile,
51
+ templates: callTemplates
52
+ }, call.callbackArg);
53
+
54
+ replacements.push({
55
+ code: `new ${NAMESPACE}.ArraySlot(\n ${call.arrayArg.getText(sourceFile)},\n ${rewrittenCallback}\n )`,
56
+ end: call.node.end,
57
+ start: call.node.getStart(sourceFile)
58
+ });
59
+ }
60
+
61
+ for (let [html, id] of callTemplates) {
62
+ prepend.push(`const ${id} = ${NAMESPACE}.template(\`${html}\`);`);
63
+ }
64
+
65
+ if (templates.length > 0) {
66
+ let result = generateCode(templates, sourceFile, undefined, callRanges);
67
+
68
+ prepend.push(...result.prepend);
69
+
70
+ for (let i = 0, n = result.replacements.length; i < n; i++) {
71
+ let r = result.replacements[i];
72
+
73
+ replacements.push({
74
+ code: r.generate(sourceFile),
75
+ end: r.node.end,
76
+ start: r.node.getStart(sourceFile)
77
+ });
78
+ }
79
+
80
+ imports.push('html');
81
+ }
82
+
83
+ // Apply replacements to source in reverse order to preserve positions
84
+ let output = source;
85
+
86
+ replacements.sort((a, b) => b.start - a.start);
87
+
88
+ for (let i = 0, n = replacements.length; i < n; i++) {
89
+ let r = replacements[i];
90
+
91
+ output = output.slice(0, r.start) + r.code + output.slice(r.end);
92
+ }
93
+
94
+ // Prepend template factories
95
+ if (prepend.length > 0) {
96
+ output = prepend.join('\n') + '\n' + output;
97
+ }
98
+
99
+ return { imports, output, prepend, replacements };
100
+ }
101
+
102
+
103
+ describe('compiler/integration', () => {
104
+ describe('static template - full pipeline', () => {
105
+ it('produces correct replacement code for static template', () => {
106
+ let source = `import { html } from '@esportsplus/template';\nlet el = html\`<div>hello</div>\`;`,
107
+ { imports, output, prepend } = pipeline(source);
108
+
109
+ expect(imports).toContain('html');
110
+ expect(prepend.length).toBe(1);
111
+ expect(prepend[0]).toContain(`${NAMESPACE}.template(\`<div>hello</div>\`)`);
112
+ expect(output).not.toContain('html`');
113
+ expect(output).toContain(`${NAMESPACE}.template(`);
114
+ });
115
+ });
116
+
117
+ describe('template with expressions - full pipeline', () => {
118
+ it('produces slot bindings for expressions', () => {
119
+ let source = `import { html } from '@esportsplus/template';\nlet el = html\`<div>\${value}</div>\`;`,
120
+ { output, prepend } = pipeline(source);
121
+
122
+ expect(prepend.length).toBe(1);
123
+ expect(output).toContain(`${NAMESPACE}.slot(`);
124
+ expect(output).toContain('value');
125
+ expect(output).not.toContain('html`');
126
+ });
127
+ });
128
+
129
+ describe('template with events - full pipeline', () => {
130
+ it('removes events from HTML and generates delegation code', () => {
131
+ let source = `import { html } from '@esportsplus/template';\nlet el = html\`<button onclick=\${handler}>click</button>\`;`,
132
+ { output, prepend } = pipeline(source);
133
+
134
+ // Event attributes stripped from template HTML
135
+ expect(prepend.length).toBe(1);
136
+ expect(prepend[0]).not.toContain('onclick');
137
+ expect(prepend[0]).toContain(`${NAMESPACE}.template(`);
138
+
139
+ // Delegation code generated
140
+ expect(output).toContain(`${NAMESPACE}.delegate(`);
141
+ expect(output).toContain("'click'");
142
+ expect(output).toContain('handler');
143
+ });
144
+ });
145
+
146
+ describe('multiple templates in one file - full pipeline', () => {
147
+ it('compiles all templates correctly', () => {
148
+ let source = [
149
+ `import { html } from '@esportsplus/template';`,
150
+ `let a = html\`<div>first</div>\`;`,
151
+ `let b = html\`<span>second</span>\`;`
152
+ ].join('\n'),
153
+ { output, prepend, replacements } = pipeline(source);
154
+
155
+ // Two different templates = two factories
156
+ expect(prepend.length).toBe(2);
157
+ expect(replacements.length).toBe(2);
158
+
159
+ // Both rewritten - no html`` left
160
+ expect(output).not.toContain('html`');
161
+
162
+ // Both template factories present
163
+ let hasFirst = prepend.some(p => p.includes('<div>first</div>')),
164
+ hasSecond = prepend.some(p => p.includes('<span>second</span>'));
165
+
166
+ expect(hasFirst).toBe(true);
167
+ expect(hasSecond).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe('nested templates - full pipeline', () => {
172
+ it('compiles outer and inner templates', () => {
173
+ let source = `import { html } from '@esportsplus/template';\nlet el = html\`<div>\${html\`<span>inner</span>\`}</div>\`;`,
174
+ { output, prepend } = pipeline(source);
175
+
176
+ // Outer + inner = 2 template factories
177
+ expect(prepend.length).toBe(2);
178
+
179
+ let hasOuter = prepend.some(p => p.includes('<div>')),
180
+ hasInner = prepend.some(p => p.includes('<span>inner</span>'));
181
+
182
+ expect(hasOuter).toBe(true);
183
+ expect(hasInner).toBe(true);
184
+
185
+ // Nested html`` rewritten
186
+ expect(output).not.toContain('html`');
187
+ expect(output).toContain('insertBefore');
188
+ });
189
+ });
190
+
191
+ describe('html.reactive() - full pipeline', () => {
192
+ it('generates ArraySlot binding', () => {
193
+ let source = `import { html } from '@esportsplus/template';\nlet el = html.reactive(items, (item) => html\`<li>\${item}</li>\`);`,
194
+ { output, prepend } = pipeline(source);
195
+
196
+ // Template factory for callback template
197
+ expect(prepend.length).toBeGreaterThanOrEqual(1);
198
+
199
+ let hasLi = prepend.some(p => p.includes('<li>'));
200
+
201
+ expect(hasLi).toBe(true);
202
+
203
+ // ArraySlot generated
204
+ expect(output).toContain(`${NAMESPACE}.ArraySlot`);
205
+ expect(output).toContain('items');
206
+ expect(output).not.toContain('html.reactive');
207
+ });
208
+ });
209
+
210
+ describe('no html templates - full pipeline', () => {
211
+ it('produces no changes for source without templates', () => {
212
+ let source = `let x = 1;\nlet y = 'hello';\nconsole.log(x + y);`,
213
+ { imports, output, prepend, replacements } = pipeline(source);
214
+
215
+ expect(imports).toHaveLength(0);
216
+ expect(prepend).toHaveLength(0);
217
+ expect(replacements).toHaveLength(0);
218
+ expect(output).toBe(source);
219
+ });
220
+ });
221
+
222
+ describe('mixed content - full pipeline', () => {
223
+ it('only rewrites templates, preserves surrounding code', () => {
224
+ let source = [
225
+ `import { html } from '@esportsplus/template';`,
226
+ `import { signal } from '@esportsplus/reactivity';`,
227
+ ``,
228
+ `let count = signal(0);`,
229
+ `let label = 'Counter';`,
230
+ ``,
231
+ `let el = html\`<div class=\${cls}>\${() => count()}</div>\`;`,
232
+ ``,
233
+ `console.log('done');`
234
+ ].join('\n'),
235
+ { output, prepend } = pipeline(source);
236
+
237
+ // Template factory generated
238
+ expect(prepend.length).toBe(1);
239
+
240
+ // Non-template code preserved
241
+ expect(output).toContain(`import { signal } from '@esportsplus/reactivity';`);
242
+ expect(output).toContain(`let count = signal(0);`);
243
+ expect(output).toContain(`let label = 'Counter';`);
244
+ expect(output).toContain(`console.log('done');`);
245
+
246
+ // Template rewritten
247
+ expect(output).not.toContain('html`');
248
+ expect(output).toContain(`${NAMESPACE}.setList(`);
249
+ expect(output).toContain(`${NAMESPACE}.EffectSlot`);
250
+ });
251
+ });
252
+ });