@sprlab/wccompiler 0.12.1 → 0.14.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/lib/codegen.js CHANGED
@@ -1,2029 +1,2074 @@
1
- /**
2
- * Code Generator for wcCompiler v2.
3
- *
4
- * Takes a complete ParseResult (with bindings, events populated by tree-walker)
5
- * and produces a self-contained JavaScript string with:
6
- * - Inline mini reactive runtime (zero imports)
7
- * - Scoped CSS injection
8
- * - HTMLElement class with signals, computeds, effects, events
9
- * - customElements.define registration
10
- *
11
- * This is a simplified version of v1's codegen, scoped to core features only:
12
- * signals, computeds, effects, text interpolation, event bindings, user methods.
13
- * No props, emits, slots, if, for, model, show, attr, refs, or lifecycle hooks.
14
- */
15
-
16
- import { reactiveRuntime, buildInlineRuntime } from './reactive-runtime.js';
17
- import { scopeCSS } from './css-scoper.js';
18
- import { camelToKebab } from './parser-extractors.js';
19
-
20
- /** @import { ParseResult } from './types.js' */
21
-
22
- /**
23
- * Convert a path array to a JS expression string.
24
- * e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
25
- *
26
- * @param {string[]} parts
27
- * @param {string} rootVar
28
- * @returns {string}
29
- */
30
- export function pathExpr(parts, rootVar) {
31
- return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
32
- }
33
-
34
- /**
35
- * Escape special regex characters in a string.
36
- *
37
- * @param {string} str
38
- * @returns {string}
39
- */
40
- function escapeRegex(str) {
41
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
- }
43
-
44
- /**
45
- * Get the signal reference for a slot prop source expression.
46
- *
47
- * @param {string} source — Source variable name from :prop="source"
48
- * @param {string[]} signalNames — Signal variable names
49
- * @param {string[]} computedNames — Computed variable names
50
- * @param {Set<string>} propNames — Prop names from defineProps
51
- * @returns {string}
52
- */
53
- function slotPropRef(source, signalNames, computedNames, propNames) {
54
- if (propNames.has(source)) return `this._s_${source}()`;
55
- if (computedNames.includes(source)) return `this._c_${source}()`;
56
- if (signalNames.includes(source)) return `this._${source}()`;
57
- return `'${source}'`;
58
- }
59
-
60
- /**
61
- * Transform an expression by rewriting signal/computed variable references
62
- * to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
63
- *
64
- * Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
65
- * Also handles `emitsObjectName(` → `this._emit(` transformation.
66
- *
67
- * Uses word-boundary regex for each known signal/computed name.
68
- * Does NOT transform if the name is followed by `.set(` (that's a write,
69
- * handled by transformMethodBody).
70
- *
71
- * @param {string} expr — Expression to transform
72
- * @param {string[]} signalNames — Signal variable names
73
- * @param {string[]} computedNames — Computed variable names
74
- * @param {string|null} [propsObjectName] — Props object variable name
75
- * @param {Set<string>} [propNames] — Set of prop names
76
- * @param {string|null} [emitsObjectName] — Emits object variable name
77
- * @returns {string}
78
- */
79
- export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = [], modelVarMap = new Map()) {
80
- let result = expr;
81
-
82
- // Transform emit calls: emitsObjectName( → this._emit(
83
- if (emitsObjectName) {
84
- const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
85
- result = result.replace(emitsRe, 'this._emit(');
86
- }
87
-
88
- // Transform method calls: methodName( → this._methodName(
89
- for (const name of methodNames) {
90
- if (propsObjectName && name === propsObjectName) continue;
91
- if (emitsObjectName && name === emitsObjectName) continue;
92
- const methodRe = new RegExp(`\\b${name}\\(`, 'g');
93
- result = result.replace(methodRe, `this._${name}(`);
94
- }
95
-
96
- // Transform props.x → this._s_x() BEFORE signal/computed transforms
97
- if (propsObjectName && propNames.size > 0) {
98
- const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
99
- result = result.replace(propsRe, (match, propName) => {
100
- if (propNames.has(propName)) {
101
- return `this._s_${propName}()`;
102
- }
103
- return match; // leave unknown props unchanged
104
- });
105
- }
106
-
107
- // Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
108
- for (const propName of propNames) {
109
- if (propsObjectName && propName === propsObjectName) continue;
110
- if (emitsObjectName && propName === emitsObjectName) continue;
111
- const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
112
- result = result.replace(bareRe, `this._s_${propName}()`);
113
- }
114
-
115
- // Transform model signal reads: varName() → this._m_{propName}() (BEFORE regular signals)
116
- for (const [varName, propNameVal] of modelVarMap) {
117
- if (propsObjectName && varName === propsObjectName) continue;
118
- if (emitsObjectName && varName === emitsObjectName) continue;
119
- // First: transform varName() calls → this._m_propName()
120
- const callRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
121
- result = result.replace(callRe, `this._m_${propNameVal}()`);
122
- // Then: transform bare varName references (not followed by ( or .set()) → this._m_propName()
123
- const bareRe = new RegExp(`\\b(${varName})\\b(?!\\.set\\()(?!\\()`, 'g');
124
- result = result.replace(bareRe, `this._m_${propNameVal}()`);
125
- }
126
-
127
- // Transform computed names first (to avoid partial matches with signals)
128
- for (const name of computedNames) {
129
- // Skip propsObjectName and emitsObjectName
130
- if (propsObjectName && name === propsObjectName) continue;
131
- if (emitsObjectName && name === emitsObjectName) continue;
132
- // First: transform name() calls → this._c_name() (replace the call, not just the name)
133
- const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
134
- result = result.replace(callRe, `this._c_${name}()`);
135
- // Then: transform bare name references (not followed by ( or .set() → this._c_name()
136
- const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
137
- result = result.replace(bareRe, `this._c_${name}()`);
138
- }
139
-
140
- // Transform signal names
141
- for (const name of signalNames) {
142
- // Skip propsObjectName and emitsObjectName
143
- if (propsObjectName && name === propsObjectName) continue;
144
- if (emitsObjectName && name === emitsObjectName) continue;
145
- // First: transform name() calls → this._name() (replace the call, not just the name)
146
- const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
147
- result = result.replace(callRe, `this._${name}()`);
148
- // Then: transform bare name references (not followed by ( or .set() → this._name()
149
- const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
150
- result = result.replace(bareRe, `this._${name}()`);
151
- }
152
-
153
- // Transform constant names → this._const_name (no function call)
154
- for (const name of constantNames) {
155
- if (propsObjectName && name === propsObjectName) continue;
156
- if (emitsObjectName && name === emitsObjectName) continue;
157
- const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
158
- result = result.replace(bareRe, `this._const_${name}`);
159
- }
160
-
161
- return result;
162
- }
163
-
164
- /**
165
- * Transform a method/effect body by rewriting signal writes and reads.
166
- *
167
- * - `emitsObjectName(` → `this._emit(` (emit call)
168
- * - `props.x` → `this._s_x()` (prop access)
169
- * - `varName.set(value)` → `this._modelSet_{propName}(value)` (model signal write)
170
- * - `x.set(value)` → `this._x(value)` (signal write via setter)
171
- * - `varName()` → `this._m_{propName}()` (model signal read)
172
- * - `x()` → `this._x()` (signal read)
173
- * - Computed `x()` → `this._c_x()` (computed read)
174
- *
175
- * @param {string} body — Function body to transform
176
- * @param {string[]} signalNames — Signal variable names
177
- * @param {string[]} computedNames — Computed variable names
178
- * @param {string|null} [propsObjectName] — Props object variable name
179
- * @param {Set<string>} [propNames] — Set of prop names
180
- * @param {string|null} [emitsObjectName] — Emits object variable name
181
- * @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
182
- * @param {string[]} [constantNames] — Constant variable names
183
- * @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
184
- * @returns {string}
185
- */
186
- export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = [], modelVarMap = new Map()) {
187
- let result = body;
188
-
189
- // 0a. Transform emit calls: emitsObjectName( → this._emit(
190
- if (emitsObjectName) {
191
- const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
192
- result = result.replace(emitsRe, 'this._emit(');
193
- }
194
-
195
- // 0b. Transform props.x → this._s_x() BEFORE other transforms
196
- if (propsObjectName && propNames.size > 0) {
197
- const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
198
- result = result.replace(propsRe, (match, propName) => {
199
- if (propNames.has(propName)) {
200
- return `this._s_${propName}()`;
201
- }
202
- return match;
203
- });
204
- }
205
-
206
- // 0c. Transform ref access: varName.value → this._varName.value
207
- for (const name of refVarNames) {
208
- const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
209
- result = result.replace(refRe, `this._${name}.value`);
210
- }
211
-
212
- // 0d. Transform model signal writes: varName.set(expr) → this._modelSet_{propName}(expr)
213
- // Must run BEFORE regular signal .set() transforms
214
- for (const [varName, propNameVal] of modelVarMap) {
215
- if (propsObjectName && varName === propsObjectName) continue;
216
- if (emitsObjectName && varName === emitsObjectName) continue;
217
- const setRe = new RegExp(`\\b${varName}\\.set\\(`, 'g');
218
- result = result.replace(setRe, `this._modelSet_${propNameVal}(`);
219
- }
220
-
221
- // 1. Transform signal writes: x.set(value) → this._x(value)
222
- for (const name of signalNames) {
223
- if (propsObjectName && name === propsObjectName) continue;
224
- if (emitsObjectName && name === emitsObjectName) continue;
225
- const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
226
- result = result.replace(setRe, `this._${name}(`);
227
- }
228
-
229
- // 1b. Transform model signal reads: varName() → this._m_{propName}()
230
- // Must run BEFORE regular signal read transforms
231
- for (const [varName, propNameVal] of modelVarMap) {
232
- if (propsObjectName && varName === propsObjectName) continue;
233
- if (emitsObjectName && varName === emitsObjectName) continue;
234
- const readRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
235
- result = result.replace(readRe, `this._m_${propNameVal}()`);
236
- }
237
-
238
- // 2. Transform computed reads: x() → this._c_x()
239
- for (const name of computedNames) {
240
- if (propsObjectName && name === propsObjectName) continue;
241
- if (emitsObjectName && name === emitsObjectName) continue;
242
- const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
243
- result = result.replace(readRe, `this._c_${name}()`);
244
- }
245
-
246
- // 3. Transform signal reads: x() → this._x()
247
- for (const name of signalNames) {
248
- if (propsObjectName && name === propsObjectName) continue;
249
- if (emitsObjectName && name === emitsObjectName) continue;
250
- const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
251
- result = result.replace(readRe, `this._${name}()`);
252
- }
253
-
254
- // 4. Transform constant reads: name → this._const_name
255
- for (const name of constantNames) {
256
- if (propsObjectName && name === propsObjectName) continue;
257
- if (emitsObjectName && name === emitsObjectName) continue;
258
- const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
259
- result = result.replace(bareRe, `this._const_${name}`);
260
- }
261
-
262
- return result;
263
- }
264
-
265
- /**
266
- * Transform an expression within the scope of an each block.
267
- * - References to itemVar and indexVar are left UNTRANSFORMED
268
- * - References to component variables (props, reactive, computed) ARE transformed
269
- *
270
- * @param {string} expr - The expression to transform
271
- * @param {string} itemVar - Name of the iteration variable
272
- * @param {string | null} indexVar - Name of the index variable
273
- * @param {Set<string>} propsSet
274
- * @param {Set<string>} rootVarNames - Set of signal names
275
- * @param {Set<string>} computedNames
276
- * @returns {string}
277
- */
278
- export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
279
- let r = expr;
280
- const excludeSet = new Set([itemVar]);
281
- if (indexVar) excludeSet.add(indexVar);
282
-
283
- for (const p of propsSet) {
284
- if (excludeSet.has(p)) continue;
285
- // First: transform name() calls → this._s_name() (don't double-call)
286
- r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
287
- // Then: transform bare name references
288
- r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
289
- }
290
- for (const n of rootVarNames) {
291
- if (excludeSet.has(n)) continue;
292
- // First: transform name() calls → this._name() (don't double-call)
293
- r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
294
- // Then: transform bare name references
295
- r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
296
- }
297
- for (const n of computedNames) {
298
- if (excludeSet.has(n)) continue;
299
- // First: transform name() calls → this._c_name() (don't double-call)
300
- r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
301
- // Then: transform bare name references
302
- r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
303
- }
304
- return r;
305
- }
306
-
307
- /**
308
- * Check if a binding name is static within an each scope (references only item/index).
309
- * A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
310
- *
311
- * @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
312
- * @param {string} itemVar
313
- * @param {string | null} indexVar
314
- * @returns {boolean}
315
- */
316
- export function isStaticForBinding(name, itemVar, indexVar) {
317
- if (name === itemVar || name.startsWith(itemVar + '.')) return true;
318
- if (indexVar && name === indexVar) return true;
319
- return false;
320
- }
321
-
322
- /**
323
- * Check if an expression is static within an each scope (references only item/index, no component vars).
324
- *
325
- * @param {string} expr
326
- * @param {string} itemVar
327
- * @param {string | null} indexVar
328
- * @param {Set<string>} propsSet
329
- * @param {Set<string>} rootVarNames
330
- * @param {Set<string>} computedNames
331
- * @returns {boolean}
332
- */
333
- export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
334
- const excludeSet = new Set([itemVar]);
335
- if (indexVar) excludeSet.add(indexVar);
336
-
337
- for (const p of propsSet) {
338
- if (excludeSet.has(p)) continue;
339
- if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
340
- }
341
- for (const n of rootVarNames) {
342
- if (excludeSet.has(n)) continue;
343
- if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
344
- }
345
- for (const n of computedNames) {
346
- if (excludeSet.has(n)) continue;
347
- if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
348
- }
349
- return true;
350
- }
351
-
352
- /**
353
- * Generate the JS expression for an event handler based on its type:
354
- * - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
355
- * - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
356
- * - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
357
- *
358
- * @param {string} handler — The raw handler string from the template
359
- * @param {string[]} signalNames
360
- * @param {string[]} computedNames
361
- * @param {string|null} propsObjectName
362
- * @param {Set<string>} propNames
363
- * @param {string|null} emitsObjectName
364
- * @param {string[]} constantNames
365
- * @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
366
- * @returns {string}
367
- */
368
- export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap = new Map()) {
369
- if (handler.includes('=>')) {
370
- // Arrow function expression: (e) => removeItem(item)
371
- const arrowIdx = handler.indexOf('=>');
372
- const params = handler.slice(0, arrowIdx).trim();
373
- let body = handler.slice(arrowIdx + 2).trim();
374
- body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames, modelVarMap);
375
- return `${params} => { ${body}; }`;
376
- } else if (handler.includes('(')) {
377
- // Function call expression: removeItem(item)
378
- const parenIdx = handler.indexOf('(');
379
- const fnName = handler.slice(0, parenIdx).trim();
380
- const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
381
- const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap) : '';
382
- return `(e) => { this._${fnName}(${transformedArgs}); }`;
383
- } else {
384
- // Simple method name
385
- return `this._${handler}.bind(this)`;
386
- }
387
- }
388
-
389
- /**
390
- * Generate the JS expression for an event handler inside an each block.
391
- * Similar to generateEventHandler but uses transformForExpr for the each scope.
392
- *
393
- * @param {string} handler
394
- * @param {string} itemVar
395
- * @param {string|null} indexVar
396
- * @param {Set<string>} propNames
397
- * @param {Set<string>} signalNamesSet
398
- * @param {Set<string>} computedNamesSet
399
- * @returns {string}
400
- */
401
- export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
402
- if (handler.includes('=>')) {
403
- // Arrow function expression
404
- const arrowIdx = handler.indexOf('=>');
405
- const params = handler.slice(0, arrowIdx).trim();
406
- let body = handler.slice(arrowIdx + 2).trim();
407
- body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
408
- return `${params} => { ${body}; }`;
409
- } else if (handler.includes('(')) {
410
- // Function call expression: removeItem(item)
411
- const parenIdx = handler.indexOf('(');
412
- const fnName = handler.slice(0, parenIdx).trim();
413
- const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
414
- const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
415
- return `(e) => { this._${fnName}(${transformedArgs}); }`;
416
- } else {
417
- // Simple method name
418
- return `this._${handler}.bind(this)`;
419
- }
420
- }
421
-
422
- /**
423
- * Generate per-item setup code for bindings, events, show, attr, model, and slots.
424
- * Used by both keyed and non-keyed each effects.
425
- *
426
- * @param {string[]} lines — Output lines array
427
- * @param {object} forBlock — ForBlock with bindings, events, etc.
428
- * @param {string} itemVar
429
- * @param {string|null} indexVar
430
- * @param {Set<string>} propNames
431
- * @param {Set<string>} signalNamesSet
432
- * @param {Set<string>} computedNamesSet
433
- */
434
- function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
435
- const indent = ' ';
436
-
437
- // Bindings
438
- for (const b of forBlock.bindings) {
439
- const nodeRef = pathExpr(b.path, 'node');
440
- if (isStaticForBinding(b.name, itemVar, indexVar)) {
441
- lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
442
- } else {
443
- const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
444
- lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
445
- }
446
- }
447
-
448
- // Events
449
- for (const e of forBlock.events) {
450
- const nodeRef = pathExpr(e.path, 'node');
451
- const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
452
- lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
453
- }
454
-
455
- // Show
456
- for (const sb of forBlock.showBindings) {
457
- const nodeRef = pathExpr(sb.path, 'node');
458
- if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
459
- lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
460
- } else {
461
- const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
462
- lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
463
- }
464
- }
465
-
466
- // Attr bindings
467
- for (const ab of forBlock.attrBindings) {
468
- const nodeRef = pathExpr(ab.path, 'node');
469
- if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
470
- lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
471
- lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
472
- } else {
473
- const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
474
- lines.push(`${indent} __effect(() => {`);
475
- lines.push(`${indent} const __val = ${expr};`);
476
- lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
477
- lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
478
- lines.push(`${indent} });`);
479
- }
480
- }
481
-
482
- // Model bindings
483
- for (const mb of (forBlock.modelBindings || [])) {
484
- const nodeRef = pathExpr(mb.path, 'node');
485
- lines.push(`${indent} __effect(() => {`);
486
- if (mb.prop === 'checked' && mb.radioValue !== null) {
487
- lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
488
- } else if (mb.prop === 'checked') {
489
- lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
490
- } else {
491
- lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
492
- }
493
- lines.push(`${indent} });`);
494
- if (mb.prop === 'checked' && mb.radioValue === null) {
495
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
496
- } else if (mb.coerce) {
497
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
498
- } else {
499
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
500
- }
501
- }
502
-
503
- // Scoped slots
504
- for (const s of (forBlock.slots || [])) {
505
- if (s.slotProps.length > 0) {
506
- const slotNodeRef = pathExpr(s.path, 'node');
507
- const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
508
- lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
509
- lines.push(`${indent} const __sp = { ${propsEntries} };`);
510
- lines.push(`${indent} let __h = __slotEl.innerHTML;`);
511
- lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
512
- lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '(\\\\(\\\\))?\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
513
- lines.push(`${indent} }`);
514
- lines.push(`${indent} __slotEl.innerHTML = __h;`);
515
- lines.push(`${indent} }`);
516
- }
517
- }
518
-
519
- // Nested each directives (forBlocks)
520
- for (const innerFor of (forBlock.forBlocks || [])) {
521
- const innerVn = innerFor.varName;
522
- const innerItemVar = innerFor.itemVar;
523
- const innerIndexVar = innerFor.indexVar;
524
- const innerSource = innerFor.source;
525
- const innerKeyExpr = innerFor.keyExpr;
526
-
527
- // Build excludeSet that includes BOTH outer and inner loop variables
528
- // so transformForExpr does not rewrite them as signals
529
- const outerExcludeVars = [itemVar];
530
- if (indexVar) outerExcludeVars.push(indexVar);
531
- const innerExcludeVars = [innerItemVar];
532
- if (innerIndexVar) innerExcludeVars.push(innerIndexVar);
533
-
534
- // Create inner template element
535
- lines.push(`${indent} const ${innerVn}_tpl = document.createElement('template');`);
536
- lines.push(`${indent} ${innerVn}_tpl.innerHTML = \`${innerFor.templateHtml}\`;`);
537
-
538
- // Find inner anchor comment in the cloned outer item node
539
- lines.push(`${indent} const ${innerVn}_anchor = ${pathExpr(innerFor.anchorPath, 'node')};`);
540
-
541
- // Transform the inner source expression (may reference outer item var)
542
- const innerSourceExpr = transformForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
543
-
544
- // Determine if inner source is static (only references outer loop vars)
545
- const innerSourceIsStatic = isStaticForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
546
-
547
- if (innerKeyExpr) {
548
- // ── Keyed reconciliation for nested each ──
549
- lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
550
- lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
551
- lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
552
- lines.push(`${indent} : (${innerVn}_source || []);`);
553
- lines.push(`${indent} const ${innerVn}_newNodes = [];`);
554
- lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
555
- lines.push(`${indent} const __key = ${innerKeyExpr};`);
556
- lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
557
- lines.push(`${indent} const innerNode = clone.firstChild;`);
558
-
559
- // Generate inner item bindings with combined excludeSet
560
- generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
561
-
562
- lines.push(`${indent} ${innerVn}_newNodes.push(innerNode);`);
563
- lines.push(`${indent} });`);
564
- lines.push(`${indent} for (const n of ${innerVn}_newNodes) { ${innerVn}_anchor.parentNode.insertBefore(n, ${innerVn}_anchor); }`);
565
- } else {
566
- // ── Non-keyed nested each: iterate and clone ──
567
- lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
568
- lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
569
- lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
570
- lines.push(`${indent} : (${innerVn}_source || []);`);
571
- lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
572
- lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
573
- lines.push(`${indent} const innerNode = clone.firstChild;`);
574
-
575
- // Generate inner item bindings with combined excludeSet
576
- generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
577
-
578
- lines.push(`${indent} ${innerVn}_anchor.parentNode.insertBefore(innerNode, ${innerVn}_anchor);`);
579
- lines.push(`${indent} });`);
580
- }
581
- }
582
-
583
- // Nested if/else-if/else chains (ifBlocks)
584
- for (const ifBlock of (forBlock.ifBlocks || [])) {
585
- const vn = ifBlock.varName;
586
- const branches = ifBlock.branches;
587
-
588
- // 3.1: Create template elements for each branch
589
- for (let i = 0; i < branches.length; i++) {
590
- const branch = branches[i];
591
- lines.push(`${indent} const ${vn}_t${i} = document.createElement('template');`);
592
- lines.push(`${indent} ${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
593
- }
594
-
595
- // 3.1: Find anchor comment in the cloned node
596
- lines.push(`${indent} const ${vn}_anchor = ${pathExpr(ifBlock.anchorPath, 'node')};`);
597
-
598
- // 3.2: Generate per-item conditional evaluation (static, not reactive)
599
- lines.push(`${indent} let ${vn}_branch = null;`);
600
- for (let i = 0; i < branches.length; i++) {
601
- const branch = branches[i];
602
- if (branch.type === 'if') {
603
- lines.push(`${indent} if (${branch.expression}) { ${vn}_branch = ${i}; }`);
604
- } else if (branch.type === 'else-if') {
605
- lines.push(`${indent} else if (${branch.expression}) { ${vn}_branch = ${i}; }`);
606
- } else {
607
- // else
608
- lines.push(`${indent} else { ${vn}_branch = ${i}; }`);
609
- }
610
- }
611
-
612
- // 3.3: Insert only the matching branch node and apply branch bindings/events/show/attr/model
613
- lines.push(`${indent} if (${vn}_branch !== null) {`);
614
- const tplArray = branches.map((_, i) => `${vn}_t${i}`).join(', ');
615
- lines.push(`${indent} const ${vn}_tpl = [${tplArray}][${vn}_branch];`);
616
- lines.push(`${indent} const ${vn}_clone = ${vn}_tpl.content.cloneNode(true);`);
617
- lines.push(`${indent} const ${vn}_node = ${vn}_clone.firstChild;`);
618
- lines.push(`${indent} ${vn}_anchor.parentNode.insertBefore(${vn}_node, ${vn}_anchor);`);
619
-
620
- // Apply branch bindings/events/show/attr/model using the outer loop's item variable
621
- const hasSetup = branches.some(b =>
622
- (b.bindings && b.bindings.length > 0) ||
623
- (b.events && b.events.length > 0) ||
624
- (b.showBindings && b.showBindings.length > 0) ||
625
- (b.attrBindings && b.attrBindings.length > 0) ||
626
- (b.modelBindings && b.modelBindings.length > 0)
627
- );
628
- if (hasSetup) {
629
- // Generate per-branch setup inline (static evaluation using item variable)
630
- for (let i = 0; i < branches.length; i++) {
631
- const branch = branches[i];
632
- const hasBranchSetup =
633
- (branch.bindings && branch.bindings.length > 0) ||
634
- (branch.events && branch.events.length > 0) ||
635
- (branch.showBindings && branch.showBindings.length > 0) ||
636
- (branch.attrBindings && branch.attrBindings.length > 0) ||
637
- (branch.modelBindings && branch.modelBindings.length > 0);
638
- if (!hasBranchSetup) continue;
639
-
640
- const keyword = i === 0 ? 'if' : 'else if';
641
- lines.push(`${indent} ${keyword} (${vn}_branch === ${i}) {`);
642
-
643
- // Bindings (static: use item var directly)
644
- for (const b of branch.bindings) {
645
- const nodeRef = pathExpr(b.path, `${vn}_node`);
646
- if (isStaticForBinding(b.name, itemVar, indexVar)) {
647
- lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
648
- } else {
649
- const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
650
- lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
651
- }
652
- }
653
-
654
- // Events
655
- for (const e of branch.events) {
656
- const nodeRef = pathExpr(e.path, `${vn}_node`);
657
- const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
658
- lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
659
- }
660
-
661
- // Show bindings
662
- for (const sb of (branch.showBindings || [])) {
663
- const nodeRef = pathExpr(sb.path, `${vn}_node`);
664
- if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
665
- lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
666
- } else {
667
- const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
668
- lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
669
- }
670
- }
671
-
672
- // Attr bindings
673
- for (const ab of (branch.attrBindings || [])) {
674
- const nodeRef = pathExpr(ab.path, `${vn}_node`);
675
- if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
676
- lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
677
- lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
678
- } else {
679
- const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
680
- lines.push(`${indent} __effect(() => {`);
681
- lines.push(`${indent} const __val = ${expr};`);
682
- lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
683
- lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
684
- lines.push(`${indent} });`);
685
- }
686
- }
687
-
688
- // Model bindings
689
- for (const mb of (branch.modelBindings || [])) {
690
- const nodeRef = pathExpr(mb.path, `${vn}_node`);
691
- lines.push(`${indent} __effect(() => {`);
692
- if (mb.prop === 'checked' && mb.radioValue !== null) {
693
- lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
694
- } else if (mb.prop === 'checked') {
695
- lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
696
- } else {
697
- lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
698
- }
699
- lines.push(`${indent} });`);
700
- if (mb.prop === 'checked' && mb.radioValue === null) {
701
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
702
- } else if (mb.coerce) {
703
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
704
- } else {
705
- lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
706
- }
707
- }
708
-
709
- lines.push(`${indent} }`);
710
- }
711
- }
712
- lines.push(`${indent} }`);
713
- }
714
- }
715
-
716
- /**
717
- * Generate inner item bindings/events/show/attr/model for a nested each directive.
718
- * Uses transformForExpr with an excludeSet that includes BOTH outer and inner loop variables.
719
- *
720
- * @param {string[]} lines - Output lines array
721
- * @param {ForBlock} innerFor - The nested ForBlock
722
- * @param {string} outerItemVar - Outer loop item variable
723
- * @param {string|null} outerIndexVar - Outer loop index variable
724
- * @param {string} innerItemVar - Inner loop item variable
725
- * @param {string|null} innerIndexVar - Inner loop index variable
726
- * @param {Set<string>} propNames - Prop names set
727
- * @param {Set<string>} signalNamesSet - Signal names set
728
- * @param {Set<string>} computedNamesSet - Computed names set
729
- * @param {string} indent - Current indentation
730
- */
731
- function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent) {
732
- // Build combined exclude set with both outer and inner loop variables
733
- const combinedExcludeItemVar = innerItemVar;
734
- const combinedExcludeIndexVar = innerIndexVar;
735
-
736
- // For transformForExpr, we need to ensure both outer and inner vars are excluded.
737
- // We create a modified propNames/signalNamesSet/computedNamesSet that doesn't include
738
- // any of the loop variables. transformForExpr already excludes itemVar/indexVar,
739
- // but we also need to exclude the outer loop variables.
740
- // Strategy: filter out outer loop vars from the sets passed to transformForExpr
741
- const filteredSignalNames = new Set([...signalNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
742
- const filteredComputedNames = new Set([...computedNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
743
- const filteredPropNames = new Set([...propNames].filter(n => n !== outerItemVar && n !== outerIndexVar));
744
-
745
- // Helper: check if expression is static (only references inner/outer loop vars, no signals/computeds/props)
746
- function isNestedStatic(expr) {
747
- // An expression is static if it only references the loop variables (outer + inner)
748
- const allExclude = new Set([innerItemVar, outerItemVar]);
749
- if (innerIndexVar) allExclude.add(innerIndexVar);
750
- if (outerIndexVar) allExclude.add(outerIndexVar);
751
-
752
- for (const p of propNames) {
753
- if (allExclude.has(p)) continue;
754
- if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
755
- }
756
- for (const n of signalNamesSet) {
757
- if (allExclude.has(n)) continue;
758
- if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
759
- }
760
- for (const n of computedNamesSet) {
761
- if (allExclude.has(n)) continue;
762
- if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
763
- }
764
- return true;
765
- }
766
-
767
- // Helper: transform expression excluding both outer and inner loop vars
768
- function transformNested(expr) {
769
- return transformForExpr(expr, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
770
- }
771
-
772
- // Bindings
773
- for (const b of innerFor.bindings) {
774
- const nodeRef = pathExpr(b.path, 'innerNode');
775
- if (isNestedStatic(b.name)) {
776
- lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
777
- } else {
778
- const expr = transformNested(b.name);
779
- lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
780
- }
781
- }
782
-
783
- // Events
784
- for (const e of innerFor.events) {
785
- const nodeRef = pathExpr(e.path, 'innerNode');
786
- const handlerExpr = generateForEventHandler(e.handler, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
787
- lines.push(`${indent}${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
788
- }
789
-
790
- // Show
791
- for (const sb of innerFor.showBindings) {
792
- const nodeRef = pathExpr(sb.path, 'innerNode');
793
- if (isNestedStatic(sb.expression)) {
794
- lines.push(`${indent}${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
795
- } else {
796
- const expr = transformNested(sb.expression);
797
- lines.push(`${indent}__effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
798
- }
799
- }
800
-
801
- // Attr bindings
802
- for (const ab of innerFor.attrBindings) {
803
- const nodeRef = pathExpr(ab.path, 'innerNode');
804
- if (isNestedStatic(ab.expression)) {
805
- lines.push(`${indent}const __val_${ab.varName} = ${ab.expression};`);
806
- lines.push(`${indent}if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
807
- } else {
808
- const expr = transformNested(ab.expression);
809
- lines.push(`${indent}__effect(() => {`);
810
- lines.push(`${indent} const __val = ${expr};`);
811
- lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
812
- lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
813
- lines.push(`${indent}});`);
814
- }
815
- }
816
-
817
- // Model bindings
818
- for (const mb of (innerFor.modelBindings || [])) {
819
- const nodeRef = pathExpr(mb.path, 'innerNode');
820
- lines.push(`${indent}__effect(() => {`);
821
- if (mb.prop === 'checked' && mb.radioValue !== null) {
822
- lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
823
- } else if (mb.prop === 'checked') {
824
- lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
825
- } else {
826
- lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
827
- }
828
- lines.push(`${indent}});`);
829
- if (mb.prop === 'checked' && mb.radioValue === null) {
830
- lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
831
- } else if (mb.coerce) {
832
- lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
833
- } else {
834
- lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
835
- }
836
- }
837
- }
838
-
839
- /**
840
- * Generate a fully self-contained JS component from a ParseResult.
841
- *
842
- * @param {ParseResult} parseResult — Complete IR with bindings/events
843
- * @param {{ runtimeImportPath?: string }} [options] — Optional generation options
844
- * @returns {string} JavaScript source code
845
- */
846
- export function generateComponent(parseResult, options = {}) {
847
- const {
848
- tagName,
849
- className,
850
- style,
851
- signals,
852
- computeds,
853
- effects,
854
- methods,
855
- bindings,
856
- events,
857
- processedTemplate,
858
- propDefs = [],
859
- propsObjectName = null,
860
- emits = [],
861
- emitsObjectName = null,
862
- ifBlocks = [],
863
- showBindings = [],
864
- forBlocks = [],
865
- onMountHooks = [],
866
- onDestroyHooks = [],
867
- onAdoptHooks = [],
868
- modelBindings = [],
869
- modelPropBindings = [],
870
- attrBindings = [],
871
- slots = [],
872
- constantVars = [],
873
- watchers = [],
874
- refs = [],
875
- refBindings = [],
876
- childComponents = [],
877
- childImports = [],
878
- exposeNames = [],
879
- modelDefs = [],
880
- } = parseResult;
881
-
882
- const signalNames = signals.map(s => s.name);
883
- const computedNames = computeds.map(c => c.name);
884
- const constantNames = constantVars.map(v => v.name);
885
- const methodNames = methods.map(m => m.name);
886
- const refVarNames = refs.map(r => r.varName);
887
- const propNames = new Set(propDefs.map(p => p.name));
888
-
889
- // Build model var name → prop name map for transform functions
890
- const modelVarMap = new Map();
891
- for (const md of modelDefs) {
892
- modelVarMap.set(md.varName, md.name);
893
- }
894
-
895
- const lines = [];
896
- const comment = options.comments ? (text) => lines.push(` // --- ${text} ---`) : () => {};
897
-
898
- // ── 0. Source comment ──
899
- if (options.sourceFile) {
900
- lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
901
- }
902
-
903
- // ── 1. Reactive runtime (shared import or inline) ──
904
- if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
905
- // Determine which runtime functions this component needs
906
- const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
907
- const needsComputed = computeds.length > 0;
908
- const needsUntrack = watchers.length > 0;
909
-
910
- if (options.runtimeImportPath) {
911
- // Tree-shake: only import what this component actually uses
912
- const usedRuntime = new Set(['__signal']); // always need __signal
913
- if (needsComputed) usedRuntime.add('__computed');
914
- if (needsEffect) usedRuntime.add('__effect');
915
- if (needsUntrack) usedRuntime.add('__untrack');
916
- const imports = [...usedRuntime].join(', ');
917
- lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
918
- } else {
919
- // Standalone: inline only the runtime functions this component needs
920
- lines.push(buildInlineRuntime({ needsComputed, needsEffect, needsBatch: false, needsUntrack }).trim());
921
- }
922
- lines.push('');
923
-
924
- // ── 1b. Child component imports ──
925
- for (const ci of childImports) {
926
- if (ci.sideEffect) {
927
- // Side-effect import: no identifier, child self-registers
928
- lines.push(`import '${ci.importPath}';`);
929
- } else {
930
- // Named import with guarded registration
931
- lines.push(`import ${ci.identifier} from '${ci.importPath}';`);
932
- lines.push(`if (!customElements.get(${ci.identifier}.__meta.tag)) customElements.define(${ci.identifier}.__meta.tag, ${ci.identifier});`);
933
- }
934
- }
935
- if (childImports.length > 0) {
936
- lines.push('');
937
- }
938
-
939
- // ── 2. CSS injection (scoped, deduplicated via id guard) ──
940
- if (style) {
941
- if (options.comments) lines.push('// ── Styles ───────────────────────────────────────────');
942
- const scoped = scopeCSS(style, tagName);
943
- const cssId = `__css_${className}`;
944
- lines.push(`if (!document.getElementById('${cssId}')) {`);
945
- lines.push(` const ${cssId} = document.createElement('style');`);
946
- lines.push(` ${cssId}.id = '${cssId}';`);
947
- lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
948
- lines.push(` document.head.appendChild(${cssId});`);
949
- lines.push('}');
950
- lines.push('');
951
- }
952
-
953
- // ── 3. Template element ──
954
- if (options.comments) lines.push('// ── Template ─────────────────────────────────────────');
955
- lines.push(`const __t_${className} = document.createElement('template');`);
956
- lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
957
- lines.push('');
958
-
959
- // ── 4. HTMLElement class ──
960
- if (options.comments) lines.push('// ── Component ────────────────────────────────────────');
961
- lines.push(`class ${className} extends HTMLElement {`);
962
-
963
- // Static observedAttributes (if props or model props exist)
964
- const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
965
- if (propDefs.length > 0 || modelDefs.length > 0) {
966
- const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
967
- // For model props, observe BOTH kebab-case AND camelCase forms
968
- // Vue sets camelCase (modelValue), native HTML uses kebab-case (model-value)
969
- const modelAttrEntries = [];
970
- for (let i = 0; i < modelDefs.length; i++) {
971
- const kebab = modelAttrNames[i];
972
- const camel = modelDefs[i].name;
973
- modelAttrEntries.push(`'${kebab}'`);
974
- // Only add camelCase if it differs from kebab-case
975
- if (kebab !== camel) {
976
- modelAttrEntries.push(`'${camel}'`);
977
- }
978
- }
979
- const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
980
- lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
981
- lines.push('');
982
- }
983
-
984
- // Static __scopedSlots array (lists slot names with reactive props)
985
- const scopedSlotNames = slots.filter(s => s.name && s.slotProps.length > 0).map(s => s.name);
986
- if (scopedSlotNames.length > 0) {
987
- const scopedArr = scopedSlotNames.map(n => `'${n}'`).join(', ');
988
- lines.push(` static __scopedSlots = [${scopedArr}];`);
989
- lines.push('');
990
- }
991
-
992
- // Static __meta — component metadata for framework adapters (React wrappers, Angular events, etc.)
993
- {
994
- const metaProps = propDefs.map(p => `{ name: '${p.name}', default: ${p.default} }`).join(', ');
995
- const metaEvents = emits.map(e => `'${e}'`).join(', ');
996
- const metaModels = modelDefs.map(m => `'${m.name}'`).join(', ');
997
- const metaSlots = slots.filter(s => s.name).map(s => `'${s.name}'`).join(', ');
998
- lines.push(` static __meta = { tag: '${tagName}', props: [${metaProps}], events: [${metaEvents}], models: [${metaModels}], slots: [${metaSlots}] };`);
999
- lines.push('');
1000
- }
1001
-
1002
- // Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
1003
- lines.push(' constructor() {');
1004
- lines.push(' super();');
1005
-
1006
- // Scoped slot storage initialization
1007
- if (scopedSlotNames.length > 0) {
1008
- lines.push(' this.__slotRenderers = {};');
1009
- lines.push(' this.__slotProps = {};');
1010
- }
1011
-
1012
- // Prop signal initialization (BEFORE user signals)
1013
- for (const p of propDefs) {
1014
- lines.push(` this._s_${p.name} = __signal(${p.default});`);
1015
- }
1016
-
1017
- // Signal initialization
1018
- for (const s of signals) {
1019
- if (s === signals[0]) comment('Signals');
1020
- lines.push(` this._${s.name} = __signal(${s.value});`);
1021
- }
1022
-
1023
- // Model signal initialization
1024
- for (const md of modelDefs) {
1025
- lines.push(` this._m_${md.name} = __signal(${md.default});`);
1026
- }
1027
-
1028
- // Constant initialization
1029
- for (const c of constantVars) {
1030
- lines.push(` this._const_${c.name} = ${c.value};`);
1031
- }
1032
-
1033
- // Computed initialization
1034
- for (const c of computeds) {
1035
- if (c === computeds[0]) comment('Computed');
1036
- const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1037
- lines.push(` this._c_${c.name} = __computed(() => ${body});`);
1038
- }
1039
-
1040
- // Watcher prev-value initialization
1041
- for (let idx = 0; idx < watchers.length; idx++) {
1042
- const w = watchers[idx];
1043
- if (w.kind === 'signal') {
1044
- // For signal watchers watching a prop, initialize with the prop's default value
1045
- // so that attributeChangedCallback changes before connectedCallback are detected
1046
- if (propNames.has(w.target)) {
1047
- const propDef = propDefs.find(p => p.name === w.target);
1048
- lines.push(` this.__prev_${w.target} = ${propDef ? propDef.default : 'undefined'};`);
1049
- } else {
1050
- lines.push(` this.__prev_${w.target} = undefined;`);
1051
- }
1052
- } else {
1053
- // For getter watchers, check if the getter references a prop (e.g., props.value)
1054
- // Initialize with the prop's default so pre-connection attribute changes are detected
1055
- const propMatch = propsObjectName ? w.target.match(new RegExp(`^${propsObjectName}\\.(\\w+)$`)) : null;
1056
- if (propMatch && propNames.has(propMatch[1])) {
1057
- const propDef = propDefs.find(p => p.name === propMatch[1]);
1058
- lines.push(` this.__prev_watch${idx} = ${propDef ? propDef.default : 'undefined'};`);
1059
- } else {
1060
- lines.push(` this.__prev_watch${idx} = undefined;`);
1061
- }
1062
- }
1063
- }
1064
-
1065
- lines.push(' }');
1066
- lines.push('');
1067
-
1068
- // connectedCallback (idempotent — safe for re-mount)
1069
- lines.push(' connectedCallback() {');
1070
- lines.push(' if (this.__connected) return;');
1071
- lines.push(' this.__connected = true;');
1072
-
1073
- // ── DOM SETUP (moved from constructor for Custom Elements spec compliance) ──
1074
-
1075
- // Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
1076
- if (slots.length > 0) {
1077
- lines.push(' const __slotMap = {};');
1078
- lines.push(' const __defaultSlotNodes = [];');
1079
- lines.push(' for (const child of Array.from(this.childNodes)) {');
1080
- lines.push(" if (child.nodeName === 'TEMPLATE') {");
1081
- lines.push(' for (const attr of child.attributes) {');
1082
- lines.push(" if (attr.name.startsWith('#')) {");
1083
- lines.push(' const slotName = attr.name.slice(1);');
1084
- lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
1085
- lines.push(' }');
1086
- lines.push(' }');
1087
- lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
1088
- // NEW: regular element with slot="name" (cross-framework support)
1089
- lines.push(" const slotName = child.getAttribute('slot');");
1090
- lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
1091
- lines.push(" child.removeAttribute('slot');");
1092
- lines.push(" child.removeAttribute('slot-props');");
1093
- lines.push(" __slotMap[slotName] = { content: propsExpr ? child.innerHTML : child.outerHTML, propsExpr };");
1094
- lines.push(" } else if (child.nodeType === 1) {");
1095
- // NEW: check for slot-template-<name> attributes (React/Angular string attribute pattern)
1096
- lines.push(" for (const attr of Array.from(child.attributes)) {");
1097
- lines.push(" if (attr.name.startsWith('slot-template-')) {");
1098
- lines.push(" const slotName = attr.name.slice('slot-template-'.length);");
1099
- lines.push(" if (!__slotMap[slotName]) {");
1100
- lines.push(" __slotMap[slotName] = { content: attr.value, propsExpr: '' };");
1101
- lines.push(" }");
1102
- lines.push(" child.removeAttribute(attr.name);");
1103
- lines.push(" }");
1104
- lines.push(" }");
1105
- lines.push(" __defaultSlotNodes.push(child);");
1106
- lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
1107
- lines.push(' __defaultSlotNodes.push(child);');
1108
- lines.push(' }');
1109
- lines.push(' }');
1110
- }
1111
-
1112
- // Clone template
1113
- lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
1114
-
1115
- // Assign DOM refs for bindings
1116
- for (const b of bindings) {
1117
- lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
1118
- }
1119
-
1120
- // Assign DOM refs for events
1121
- for (const e of events) {
1122
- lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
1123
- }
1124
-
1125
- // Assign DOM refs for show bindings
1126
- for (const sb of showBindings) {
1127
- lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
1128
- }
1129
-
1130
- // Assign DOM refs for model bindings
1131
- for (const mb of modelBindings) {
1132
- lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
1133
- }
1134
-
1135
- // Assign DOM refs for model:propName bindings
1136
- for (const mpb of modelPropBindings) {
1137
- lines.push(` this.${mpb.varName} = ${pathExpr(mpb.path, '__root')};`);
1138
- }
1139
-
1140
- // Assign DOM refs for slot placeholders
1141
- for (const s of slots) {
1142
- lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
1143
- }
1144
-
1145
- // Assign DOM refs for child component instances (only if they have prop bindings)
1146
- for (const cc of childComponents) {
1147
- if (cc.propBindings.length > 0) {
1148
- lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
1149
- }
1150
- }
1151
-
1152
- // Assign DOM refs for attr bindings (reuse ref when same path)
1153
- const attrPathMap = new Map();
1154
- for (const ab of attrBindings) {
1155
- const pathKey = ab.path.join('.');
1156
- if (attrPathMap.has(pathKey)) {
1157
- lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
1158
- } else {
1159
- lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
1160
- attrPathMap.set(pathKey, ab.varName);
1161
- }
1162
- }
1163
-
1164
- // ── if: template creation, anchor reference, state init ──
1165
- for (const ifBlock of ifBlocks) {
1166
- const vn = ifBlock.varName;
1167
- // Template per branch
1168
- for (let i = 0; i < ifBlock.branches.length; i++) {
1169
- const branch = ifBlock.branches[i];
1170
- lines.push(` this.${vn}_t${i} = document.createElement('template');`);
1171
- lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
1172
- }
1173
- // Reference to anchor comment node (must be before appendChild moves nodes out of __root)
1174
- lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
1175
- // Active branch state
1176
- lines.push(` this.${vn}_current = null;`);
1177
- lines.push(` this.${vn}_active = undefined;`);
1178
- }
1179
-
1180
- // ── each: template creation, anchor reference, nodes array ──
1181
- for (const forBlock of forBlocks) {
1182
- const vn = forBlock.varName;
1183
- lines.push(` this.${vn}_tpl = document.createElement('template');`);
1184
- lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
1185
- lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
1186
- lines.push(` this.${vn}_nodes = [];`);
1187
- }
1188
-
1189
- // ── Ref DOM reference assignments (before appendChild moves nodes) ──
1190
- for (const rb of refBindings) {
1191
- lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
1192
- }
1193
-
1194
- // Append DOM (always light DOM)
1195
- lines.push(" this.innerHTML = '';");
1196
- lines.push(' this.appendChild(__root);');
1197
-
1198
- // Static slot injection (after DOM is appended)
1199
- for (const s of slots) {
1200
- if (s.name && s.slotProps.length > 0) {
1201
- // Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
1202
- lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
1203
- if (s.defaultContent) {
1204
- lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
1205
- }
1206
- } else if (s.name) {
1207
- // Named slot: inject content directly
1208
- lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
1209
- } else {
1210
- // Default slot: inject collected child nodes
1211
- lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
1212
- }
1213
- }
1214
-
1215
- // ── Deferred slot re-check (Angular compatibility) ──
1216
- // Angular connects custom elements to DOM BEFORE projecting children.
1217
- // If no slot content was found on first pass, schedule a microtask retry.
1218
- // We save a reference to the rendered root node so the microtask can filter it out
1219
- // and only process children that were projected by the framework after connectedCallback.
1220
- if (slots.length > 0) {
1221
- lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
1222
- lines.push(' const __renderedRoot = this.firstElementChild;');
1223
- lines.push(' queueMicrotask(() => {');
1224
- lines.push(' const __sm = {};');
1225
- lines.push(' const __dn = [];');
1226
- lines.push(' for (const child of Array.from(this.childNodes)) {');
1227
- // Skip the rendered template root and any whitespace text nodes that were there before
1228
- lines.push(' if (child === __renderedRoot) continue;');
1229
- lines.push(' if (child.nodeType === 3 && !child.textContent.trim()) continue;');
1230
- lines.push(" if (child.nodeName === 'TEMPLATE') {");
1231
- lines.push(' for (const attr of child.attributes) {');
1232
- lines.push(" if (attr.name.startsWith('#')) {");
1233
- lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
1234
- lines.push(' }');
1235
- lines.push(' }');
1236
- lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
1237
- lines.push(" const sn = child.getAttribute('slot');");
1238
- lines.push(" const pe = child.getAttribute('slot-props') || '';");
1239
- lines.push(" child.removeAttribute('slot');");
1240
- lines.push(" child.removeAttribute('slot-props');");
1241
- lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
1242
- lines.push(" child.remove();");
1243
- lines.push(" } else if (child.nodeType === 1) {");
1244
- lines.push(" for (const attr of Array.from(child.attributes)) {");
1245
- lines.push(" if (attr.name.startsWith('slot-template-')) {");
1246
- lines.push(" const sn = attr.name.slice('slot-template-'.length);");
1247
- lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
1248
- lines.push(" child.removeAttribute(attr.name);");
1249
- lines.push(" }");
1250
- lines.push(" }");
1251
- lines.push(" __dn.push(child);");
1252
- lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
1253
- lines.push(" __dn.push(child);");
1254
- lines.push(' }');
1255
- lines.push(' }');
1256
- // Re-inject slots if we found content this time
1257
- lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
1258
- for (const s of slots) {
1259
- if (s.name && s.slotProps.length > 0) {
1260
- lines.push(` if (__sm['${s.name}']) {`);
1261
- lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
1262
- if (s.slotProps.length > 0 && s.slotProps[0].source) {
1263
- lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
1264
- }
1265
- lines.push(` }`);
1266
- } else if (s.name) {
1267
- lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
1268
- } else {
1269
- lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
1270
- }
1271
- }
1272
- lines.push(' }');
1273
- lines.push(' });');
1274
- lines.push(' }');
1275
- }
1276
-
1277
- // ── EFFECTS AND LISTENERS ──
1278
- lines.push(' this.__ac = new AbortController();');
1279
- lines.push(' this.__disposers = [];');
1280
- lines.push('');
1281
-
1282
- // Binding effects — one __effect per binding
1283
- if (bindings.length > 0) comment('Text bindings');
1284
- for (const b of bindings) {
1285
- if (b.type === 'prop') {
1286
- lines.push(' this.__disposers.push(__effect(() => {');
1287
- lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
1288
- lines.push(' }));');
1289
- } else if (b.type === 'signal') {
1290
- // Check if this is a model var (needs _m_ prefix instead of _)
1291
- const modelPropName = modelVarMap.get(b.name);
1292
- const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
1293
- lines.push(' this.__disposers.push(__effect(() => {');
1294
- lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
1295
- lines.push(' }));');
1296
- } else if (b.type === 'computed') {
1297
- lines.push(' this.__disposers.push(__effect(() => {');
1298
- lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
1299
- lines.push(' }));');
1300
- } else {
1301
- // method/expression type — check if it's a props.x access or a complex expression
1302
- let ref;
1303
- if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
1304
- const propName = b.name.slice(propsObjectName.length + 1);
1305
- ref = `this._s_${propName}()`;
1306
- } else {
1307
- // Use transformExpr for complex expressions (e.g. items().length, ternary)
1308
- ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1309
- }
1310
- lines.push(' this.__disposers.push(__effect(() => {');
1311
- lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
1312
- lines.push(' }));');
1313
- }
1314
- }
1315
-
1316
- // Scoped slot effects — reactive resolution of {{propName}} in consumer templates
1317
- for (const s of slots) {
1318
- if (s.name && s.slotProps.length > 0) {
1319
- const propsObj = s.slotProps.map(sp => {
1320
- const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
1321
- return `${sp.prop}: ${ref}`;
1322
- }).join(', ');
1323
- // Scoped slot effect: always compute props and notify renderers
1324
- // The effect runs regardless of whether a template was provided (Angular uses registerSlotRenderer)
1325
- lines.push(' __effect(() => {');
1326
- lines.push(` const __props = { ${propsObj} };`);
1327
- // Store current props for late-registering renderers
1328
- lines.push(` this.__slotProps['${s.name}'] = __props;`);
1329
- // Emit wcc:slot-update event
1330
- lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
1331
- // Check for registered renderer (Angular directive)
1332
- lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
1333
- lines.push(` this.__slotRenderers['${s.name}'](__props);`);
1334
- lines.push(` } else if (this.__slotTpl_${s.name}) {`);
1335
- // Fallback: template-based token replacement (WCC-to-WCC, Vue, React)
1336
- lines.push(` let __html = this.__slotTpl_${s.name};`);
1337
- lines.push(" for (const [k, v] of Object.entries(__props)) {");
1338
- lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
1339
- lines.push(' }');
1340
- lines.push(` this.${s.varName}.innerHTML = __html;`);
1341
- lines.push(' }');
1342
- lines.push(' });');
1343
- }
1344
- }
1345
-
1346
- // Child component reactive prop bindings
1347
- for (const cc of childComponents) {
1348
- for (const pb of cc.propBindings) {
1349
- let ref;
1350
- if (pb.type === 'prop') {
1351
- ref = `this._s_${pb.expr}()`;
1352
- } else if (pb.type === 'computed') {
1353
- ref = `this._c_${pb.expr}()`;
1354
- } else if (pb.type === 'signal') {
1355
- const modelPropName = modelVarMap.get(pb.expr);
1356
- ref = modelPropName ? `this._m_${modelPropName}()` : `this._${pb.expr}()`;
1357
- } else if (pb.type === 'constant') {
1358
- ref = `this._const_${pb.expr}`;
1359
- } else {
1360
- ref = `this._${pb.expr}()`;
1361
- }
1362
- lines.push(' this.__disposers.push(__effect(() => {');
1363
- lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
1364
- lines.push(' }));');
1365
- }
1366
- }
1367
-
1368
- // User effects
1369
- for (const eff of effects) {
1370
- const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1371
- lines.push(' this.__disposers.push(__effect(() => {');
1372
- // Indent each line of the body
1373
- const bodyLines = body.split('\n');
1374
- for (const line of bodyLines) {
1375
- lines.push(` ${line}`);
1376
- }
1377
- lines.push(' }));');
1378
- }
1379
-
1380
- // Watcher effects
1381
- for (let idx = 0; idx < watchers.length; idx++) {
1382
- const w = watchers[idx];
1383
- const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1384
-
1385
- if (w.kind === 'signal') {
1386
- // Determine the signal reference for the watch target
1387
- let watchRef;
1388
- if (propNames.has(w.target)) {
1389
- watchRef = `this._s_${w.target}()`;
1390
- } else if (computedNames.includes(w.target)) {
1391
- watchRef = `this._c_${w.target}()`;
1392
- } else {
1393
- watchRef = `this._${w.target}()`;
1394
- }
1395
- lines.push(' this.__disposers.push(__effect(() => {');
1396
- lines.push(` const ${w.newParam} = ${watchRef};`);
1397
- lines.push(` if (this.__prev_${w.target} !== undefined && this.__prev_${w.target} !== ${w.newParam}) {`);
1398
- if (w.oldParam) {
1399
- lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
1400
- }
1401
- lines.push(' __untrack(() => {');
1402
- const bodyLines = body.split('\n');
1403
- for (const line of bodyLines) {
1404
- lines.push(` ${line}`);
1405
- }
1406
- lines.push(' });');
1407
- lines.push(' }');
1408
- lines.push(` this.__prev_${w.target} = ${w.newParam};`);
1409
- lines.push(' }));');
1410
- } else {
1411
- // kind === 'getter' — transform the getter expression and use it directly
1412
- const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1413
- const prevName = `__prev_watch${idx}`;
1414
- lines.push(' this.__disposers.push(__effect(() => {');
1415
- lines.push(` const ${w.newParam} = ${getterExpr};`);
1416
- lines.push(` if (this.${prevName} !== undefined && this.${prevName} !== ${w.newParam}) {`);
1417
- if (w.oldParam) {
1418
- lines.push(` const ${w.oldParam} = this.${prevName};`);
1419
- }
1420
- lines.push(' __untrack(() => {');
1421
- const bodyLines2 = body.split('\n');
1422
- for (const line of bodyLines2) {
1423
- lines.push(` ${line}`);
1424
- }
1425
- lines.push(' });');
1426
- lines.push(' }');
1427
- lines.push(` this.${prevName} = ${w.newParam};`);
1428
- lines.push(' }));');
1429
- }
1430
- }
1431
-
1432
- // Event listeners (with AbortController signal for cleanup)
1433
- if (events.length > 0) comment('Event listeners');
1434
- for (const e of events) {
1435
- const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
1436
- lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
1437
- }
1438
-
1439
- // Show effects — one __effect per ShowBinding
1440
- if (showBindings.length > 0) comment('Show directives');
1441
- for (const sb of showBindings) {
1442
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1443
- lines.push(' this.__disposers.push(__effect(() => {');
1444
- lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
1445
- lines.push(' }));');
1446
- }
1447
-
1448
- // Model effects — signal → DOM (one __effect per ModelBinding)
1449
- if (modelBindings.length > 0) comment('Model bindings (signal → DOM)');
1450
- for (const mb of modelBindings) {
1451
- if (mb.prop === 'checked' && mb.radioValue !== null) {
1452
- // Radio: compare signal value to radioValue
1453
- lines.push(' this.__disposers.push(__effect(() => {');
1454
- lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
1455
- lines.push(' }));');
1456
- } else if (mb.prop === 'checked') {
1457
- // Checkbox: coerce to boolean
1458
- lines.push(' this.__disposers.push(__effect(() => {');
1459
- lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
1460
- lines.push(' }));');
1461
- } else {
1462
- // Value-based (text, number, textarea, select): nullish coalesce to ''
1463
- lines.push(' this.__disposers.push(__effect(() => {');
1464
- lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
1465
- lines.push(' }));');
1466
- }
1467
- }
1468
-
1469
- // Model event listeners — DOM → signal (with AbortController signal)
1470
- for (const mb of modelBindings) {
1471
- if (mb.prop === 'checked' && mb.radioValue === null) {
1472
- // Checkbox: read e.target.checked
1473
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); }, { signal: this.__ac.signal });`);
1474
- } else if (mb.coerce) {
1475
- // Number input: wrap in Number()
1476
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); }, { signal: this.__ac.signal });`);
1477
- } else {
1478
- // All others: read e.target.value
1479
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); }, { signal: this.__ac.signal });`);
1480
- }
1481
- }
1482
-
1483
- // model:propName effects and listeners bidirectional WCC-to-WCC binding
1484
- for (const mpb of modelPropBindings) {
1485
- // Determine the signal read/write expressions
1486
- // If the signal is a model var, use this._m_propName(); otherwise use this._signalName()
1487
- const isModelVar = modelVarMap.has(mpb.signal);
1488
- const readExpr = isModelVar
1489
- ? `this._m_${modelVarMap.get(mpb.signal)}()`
1490
- : `this._${mpb.signal}()`;
1491
- const writeExpr = isModelVar
1492
- ? `this._m_${modelVarMap.get(mpb.signal)}`
1493
- : `this._${mpb.signal}`;
1494
-
1495
- // Reactive parent child sync: set child's attribute from parent signal
1496
- const attrName = camelToKebab(mpb.propName);
1497
- lines.push(' this.__disposers.push(__effect(() => {');
1498
- lines.push(` this.${mpb.varName}.setAttribute('${attrName}', ${readExpr} ?? '');`);
1499
- lines.push(' }));');
1500
-
1501
- // Child parent sync: listen for wcc:model on child, update parent signal
1502
- lines.push(` this.${mpb.varName}.addEventListener('wcc:model', (e) => {`);
1503
- lines.push(` if (e.detail.prop === '${mpb.propName}') {`);
1504
- lines.push(` ${writeExpr}(e.detail.value);`);
1505
- lines.push(' }');
1506
- lines.push(' }, { signal: this.__ac.signal });');
1507
- }
1508
-
1509
- // Attr binding effects — one __effect per AttrBinding
1510
- for (const ab of attrBindings) {
1511
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1512
- if (ab.kind === 'attr') {
1513
- lines.push(' this.__disposers.push(__effect(() => {');
1514
- lines.push(` const __v = ${expr};`);
1515
- lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
1516
- lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
1517
- lines.push(' }));');
1518
- } else if (ab.kind === 'bool') {
1519
- lines.push(' this.__disposers.push(__effect(() => {');
1520
- lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
1521
- lines.push(' }));');
1522
- } else if (ab.kind === 'class') {
1523
- if (ab.expression.trimStart().startsWith('{')) {
1524
- // Object expression: iterate entries, classList.add/remove
1525
- lines.push(' this.__disposers.push(__effect(() => {');
1526
- lines.push(` const __obj = ${expr};`);
1527
- lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
1528
- lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
1529
- lines.push(' }');
1530
- lines.push(' }));');
1531
- } else {
1532
- // String expression: set className
1533
- lines.push(' this.__disposers.push(__effect(() => {');
1534
- lines.push(` this.${ab.varName}.className = ${expr};`);
1535
- lines.push(' }));');
1536
- }
1537
- } else if (ab.kind === 'style') {
1538
- if (ab.expression.trimStart().startsWith('{')) {
1539
- // Object expression: iterate entries, set style[key]
1540
- lines.push(' this.__disposers.push(__effect(() => {');
1541
- lines.push(` const __obj = ${expr};`);
1542
- lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
1543
- lines.push(` this.${ab.varName}.style[__k] = __val;`);
1544
- lines.push(' }');
1545
- lines.push(' }));');
1546
- } else {
1547
- // String expression: set cssText
1548
- lines.push(' this.__disposers.push(__effect(() => {');
1549
- lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
1550
- lines.push(' }));');
1551
- }
1552
- }
1553
- }
1554
-
1555
- // ── if effects ──
1556
- for (const ifBlock of ifBlocks) {
1557
- const vn = ifBlock.varName;
1558
- lines.push(' this.__disposers.push(__effect(() => {');
1559
- lines.push(' let __branch = null;');
1560
- for (let i = 0; i < ifBlock.branches.length; i++) {
1561
- const branch = ifBlock.branches[i];
1562
- if (branch.type === 'if') {
1563
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1564
- lines.push(` if (${expr}) { __branch = ${i}; }`);
1565
- } else if (branch.type === 'else-if') {
1566
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1567
- lines.push(` else if (${expr}) { __branch = ${i}; }`);
1568
- } else {
1569
- // else
1570
- lines.push(` else { __branch = ${i}; }`);
1571
- }
1572
- }
1573
- lines.push(` if (__branch === this.${vn}_active) return;`);
1574
- // Remove previous branch
1575
- lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
1576
- // Insert new branch
1577
- lines.push(' if (__branch !== null) {');
1578
- const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
1579
- lines.push(` const tpl = [${tplArray}][__branch];`);
1580
- lines.push(' const clone = tpl.content.cloneNode(true);');
1581
- lines.push(' const node = clone.firstChild;');
1582
- lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1583
- lines.push(' customElements.upgrade(node);');
1584
- lines.push(` this.${vn}_current = node;`);
1585
- // Setup bindings/events for active branch (only if any branch has bindings/events)
1586
- const hasSetup = ifBlock.branches.some(b =>
1587
- (b.bindings && b.bindings.length > 0) ||
1588
- (b.events && b.events.length > 0) ||
1589
- (b.showBindings && b.showBindings.length > 0) ||
1590
- (b.attrBindings && b.attrBindings.length > 0) ||
1591
- (b.modelBindings && b.modelBindings.length > 0)
1592
- );
1593
- if (hasSetup) {
1594
- lines.push(` this.${vn}_setup(node, __branch);`);
1595
- }
1596
- lines.push(' }');
1597
- lines.push(` this.${vn}_active = __branch;`);
1598
- lines.push(' }));');
1599
- }
1600
-
1601
- // ── each effects ──
1602
- for (const forBlock of forBlocks) {
1603
- const vn = forBlock.varName;
1604
- const { itemVar, indexVar, source, keyExpr } = forBlock;
1605
-
1606
- const signalNamesSet = new Set(signalNames);
1607
- const computedNamesSet = new Set(computedNames);
1608
-
1609
- // Transform the source expression
1610
- const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1611
-
1612
- lines.push(' this.__disposers.push(__effect(() => {');
1613
- lines.push(` const __source = ${sourceExpr};`);
1614
- lines.push('');
1615
- lines.push(" const __iter = typeof __source === 'number'");
1616
- lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
1617
- lines.push(' : (__source || []);');
1618
- lines.push('');
1619
-
1620
- if (keyExpr) {
1621
- // ── Keyed reconciliation ──
1622
- lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
1623
- lines.push(' const __newMap = new Map();');
1624
- lines.push(' const __newNodes = [];');
1625
- lines.push('');
1626
- lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1627
- lines.push(` const __key = ${keyExpr};`);
1628
- lines.push(' if (__oldMap.has(__key)) {');
1629
- lines.push(' const node = __oldMap.get(__key);');
1630
- lines.push(' __newMap.set(__key, node);');
1631
- lines.push(' __newNodes.push(node);');
1632
- lines.push(' __oldMap.delete(__key);');
1633
- lines.push(' } else {');
1634
- lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1635
- lines.push(' const node = clone.firstChild;');
1636
-
1637
- // Setup bindings/events/show/attr/model/slots for NEW nodes only
1638
- // (reused nodes keep their existing bindings)
1639
- generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1640
-
1641
- lines.push(' __newMap.set(__key, node);');
1642
- lines.push(' __newNodes.push(node);');
1643
- lines.push(' }');
1644
- lines.push(' });');
1645
- lines.push('');
1646
- lines.push(' // Remove nodes no longer in the list');
1647
- lines.push(' for (const n of __oldMap.values()) n.remove();');
1648
- lines.push('');
1649
- lines.push(' // Reorder: insert all nodes in correct order before anchor');
1650
- lines.push(` for (const n of __newNodes) { this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor); customElements.upgrade(n); }`);
1651
- lines.push('');
1652
- lines.push(` this.${vn}_nodes = __newNodes;`);
1653
- lines.push(` this.${vn}_keyMap = __newMap;`);
1654
- lines.push(' }));');
1655
- } else {
1656
- // ── Non-keyed: destroy all and recreate (original behavior) ──
1657
- lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
1658
- lines.push(` this.${vn}_nodes = [];`);
1659
- lines.push('');
1660
- lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1661
- lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1662
- lines.push(' const node = clone.firstChild;');
1663
-
1664
- generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1665
-
1666
- lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1667
- lines.push(' customElements.upgrade(node);');
1668
- lines.push(` this.${vn}_nodes.push(node);`);
1669
- lines.push(' });');
1670
- lines.push(' }));');
1671
- }
1672
- }
1673
-
1674
- // Lifecycle: onMount hooks (at the very end of connectedCallback)
1675
- for (const hook of onMountHooks) {
1676
- const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1677
- if (hook.async) {
1678
- lines.push(' ;(async () => {');
1679
- const bodyLines = body.split('\n');
1680
- for (const line of bodyLines) {
1681
- lines.push(` ${line}`);
1682
- }
1683
- lines.push(' })();');
1684
- } else {
1685
- const bodyLines = body.split('\n');
1686
- for (const line of bodyLines) {
1687
- const trimmed = line.trimEnd();
1688
- const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1689
- lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1690
- }
1691
- }
1692
- }
1693
-
1694
- // Close connectedCallback
1695
- lines.push(' }');
1696
- lines.push('');
1697
-
1698
- // disconnectedCallback (cleanup: abort listeners + dispose effects + user hooks)
1699
- lines.push(' disconnectedCallback() {');
1700
- lines.push(' this.__connected = false;');
1701
- lines.push(' this.__ac.abort();');
1702
- lines.push(' this.__disposers.forEach(d => d());');
1703
- if (onDestroyHooks.length > 0) {
1704
- for (const hook of onDestroyHooks) {
1705
- const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1706
- if (hook.async) {
1707
- lines.push(' ;(async () => {');
1708
- const bodyLines = body.split('\n');
1709
- for (const line of bodyLines) {
1710
- lines.push(` ${line}`);
1711
- }
1712
- lines.push(' })();');
1713
- } else {
1714
- const bodyLines = body.split('\n');
1715
- for (const line of bodyLines) {
1716
- const trimmed = line.trimEnd();
1717
- const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1718
- lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1719
- }
1720
- }
1721
- }
1722
- }
1723
- lines.push(' }');
1724
- lines.push('');
1725
-
1726
- // adoptedCallback (if onAdopt hooks exist)
1727
- if (onAdoptHooks.length > 0) {
1728
- lines.push(' adoptedCallback() {');
1729
- for (const hook of onAdoptHooks) {
1730
- const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1731
- if (hook.async) {
1732
- lines.push(' ;(async () => {');
1733
- const bodyLines = body.split('\n');
1734
- for (const line of bodyLines) {
1735
- lines.push(` ${line}`);
1736
- }
1737
- lines.push(' })();');
1738
- } else {
1739
- const bodyLines = body.split('\n');
1740
- for (const line of bodyLines) {
1741
- const trimmed = line.trimEnd();
1742
- const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1743
- lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1744
- }
1745
- }
1746
- }
1747
- lines.push(' }');
1748
- lines.push('');
1749
- }
1750
-
1751
- // attributeChangedCallback (if props or model props exist)
1752
- if (propDefs.length > 0 || modelDefs.length > 0) {
1753
- lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
1754
- for (const p of propDefs) {
1755
- const defaultVal = p.default;
1756
- let updateExpr;
1757
-
1758
- if (defaultVal === 'true' || defaultVal === 'false') {
1759
- // Boolean coercion: attribute presence = true
1760
- updateExpr = `this._s_${p.name}(newVal != null)`;
1761
- } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
1762
- // Number coercion
1763
- updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
1764
- } else if (defaultVal === 'undefined') {
1765
- // Undefined default — pass through
1766
- updateExpr = `this._s_${p.name}(newVal)`;
1767
- } else {
1768
- // String default — use nullish coalescing
1769
- updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
1770
- }
1771
-
1772
- lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
1773
- }
1774
-
1775
- // Model props update signal directly (NO event emission)
1776
- for (let i = 0; i < modelDefs.length; i++) {
1777
- const md = modelDefs[i];
1778
- const attrName = modelAttrNames[i];
1779
- const camelName = md.name;
1780
- const defaultVal = md.default;
1781
- let updateExpr;
1782
-
1783
- if (defaultVal === 'true' || defaultVal === 'false') {
1784
- // Boolean coercion: attribute presence = true
1785
- updateExpr = `this._m_${md.name}(newVal != null)`;
1786
- } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
1787
- // Number coercion
1788
- updateExpr = `this._m_${md.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
1789
- } else if (defaultVal === 'undefined') {
1790
- // Undefined default — pass through
1791
- updateExpr = `this._m_${md.name}(newVal)`;
1792
- } else {
1793
- // String default — use nullish coalescing
1794
- updateExpr = `this._m_${md.name}(newVal ?? ${defaultVal})`;
1795
- }
1796
-
1797
- // Handle both kebab-case (native HTML) and camelCase (Vue) attribute names
1798
- if (attrName !== camelName) {
1799
- lines.push(` if (name === '${attrName}' || name === '${camelName}') ${updateExpr};`);
1800
- } else {
1801
- lines.push(` if (name === '${attrName}') ${updateExpr};`);
1802
- }
1803
- }
1804
-
1805
- lines.push(' }');
1806
- lines.push('');
1807
-
1808
- // Public getters and setters
1809
- for (const p of propDefs) {
1810
- lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
1811
- lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
1812
- lines.push('');
1813
- }
1814
-
1815
- // Public getters and setters for model props
1816
- for (let i = 0; i < modelDefs.length; i++) {
1817
- const md = modelDefs[i];
1818
- const attrName = modelAttrNames[i];
1819
- lines.push(` get ${md.name}() { return this._m_${md.name}(); }`);
1820
- lines.push(` set ${md.name}(val) { this._m_${md.name}(val); this.setAttribute('${attrName}', String(val)); }`);
1821
- lines.push('');
1822
- }
1823
- }
1824
-
1825
- // _emit method (if emits declared)
1826
- // Emits the original event name + lowercase-no-hyphens for React 19 compatibility.
1827
- // React 19 maps `oncountchanged` → addEventListener('countchanged').
1828
- if (emits.length > 0) {
1829
- lines.push(' _emit(name, detail) {');
1830
- lines.push(' const evt = { detail, bubbles: true, composed: true };');
1831
- lines.push(' this.dispatchEvent(new CustomEvent(name, evt));');
1832
- lines.push(" const lower = name.replace(/-/g, '').toLowerCase();");
1833
- lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
1834
- lines.push(' }');
1835
- lines.push('');
1836
- }
1837
-
1838
- // _modelSet methods (one per defineModel prop emits events on internal write)
1839
- // Emits:
1840
- // 1. wcc:model — canonical event for vanilla JS, WCC-to-WCC, React adapter, Vue plugin
1841
- // 2. propNameChange — for Angular [(prop)] banana-box syntax (zero-config)
1842
- for (const md of modelDefs) {
1843
- lines.push(` _modelSet_${md.name}(newVal) {`);
1844
- lines.push(` const oldVal = this._m_${md.name}();`);
1845
- lines.push(` this._m_${md.name}(newVal);`);
1846
- lines.push(` this.dispatchEvent(new CustomEvent('wcc:model', {`);
1847
- lines.push(` detail: { prop: '${md.name}', value: newVal, oldValue: oldVal },`);
1848
- lines.push(` bubbles: true,`);
1849
- lines.push(` composed: true`);
1850
- lines.push(` }));`);
1851
- lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
1852
- lines.push(' }');
1853
- lines.push('');
1854
- }
1855
-
1856
- // __scopedSlots instance getter and registerSlotRenderer (if scoped slots exist)
1857
- if (scopedSlotNames.length > 0) {
1858
- lines.push(' get __scopedSlots() { return this.constructor.__scopedSlots || []; }');
1859
- lines.push('');
1860
- lines.push(' registerSlotRenderer(slotName, callback) {');
1861
- lines.push(' if (!this.__slotRenderers) this.__slotRenderers = {};');
1862
- lines.push(' this.__slotRenderers[slotName] = callback;');
1863
- lines.push(' if (this.__slotProps && this.__slotProps[slotName]) {');
1864
- lines.push(' callback(this.__slotProps[slotName]);');
1865
- lines.push(' }');
1866
- lines.push(' return () => {');
1867
- lines.push(' if (this.__slotRenderers) {');
1868
- lines.push(' delete this.__slotRenderers[slotName];');
1869
- lines.push(' }');
1870
- lines.push(' };');
1871
- lines.push(' }');
1872
- lines.push('');
1873
- }
1874
-
1875
- // User methods (prefixed with _)
1876
- if (methods.length > 0 && options.comments) lines.push('');
1877
- if (methods.length > 0 && options.comments) lines.push(' // --- Methods ---');
1878
- for (const m of methods) {
1879
- const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1880
- lines.push(` _${m.name}(${m.params}) {`);
1881
- const bodyLines = body.split('\n');
1882
- for (const line of bodyLines) {
1883
- lines.push(` ${line}`);
1884
- }
1885
- lines.push(' }');
1886
- lines.push('');
1887
- }
1888
-
1889
- // ── Ref getter properties ──
1890
- for (const rd of refs) {
1891
- // Find matching RefBinding
1892
- const rb = refBindings.find(b => b.refName === rd.refName);
1893
- if (rb) {
1894
- lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
1895
- lines.push('');
1896
- }
1897
- }
1898
-
1899
- // ── defineExpose: public getters/methods ──
1900
- for (const name of exposeNames) {
1901
- if (computedNames.includes(name)) {
1902
- lines.push(` get ${name}() { return this._c_${name}(); }`);
1903
- } else if (signalNames.includes(name)) {
1904
- lines.push(` get ${name}() { return this._${name}(); }`);
1905
- } else if (methodNames.includes(name)) {
1906
- lines.push(` ${name}(...args) { return this._${name}(...args); }`);
1907
- } else if (constantNames.includes(name)) {
1908
- lines.push(` get ${name}() { return this._const_${name}; }`);
1909
- }
1910
- }
1911
- if (exposeNames.length > 0) lines.push('');
1912
-
1913
- // ── if setup methods ──
1914
- for (const ifBlock of ifBlocks) {
1915
- const vn = ifBlock.varName;
1916
- const hasSetup = ifBlock.branches.some(b =>
1917
- (b.bindings && b.bindings.length > 0) ||
1918
- (b.events && b.events.length > 0) ||
1919
- (b.showBindings && b.showBindings.length > 0) ||
1920
- (b.attrBindings && b.attrBindings.length > 0) ||
1921
- (b.modelBindings && b.modelBindings.length > 0)
1922
- );
1923
- if (!hasSetup) continue;
1924
-
1925
- lines.push(` ${vn}_setup(node, branch) {`);
1926
- for (let i = 0; i < ifBlock.branches.length; i++) {
1927
- const branch = ifBlock.branches[i];
1928
- const hasBranchSetup =
1929
- (branch.bindings && branch.bindings.length > 0) ||
1930
- (branch.events && branch.events.length > 0) ||
1931
- (branch.showBindings && branch.showBindings.length > 0) ||
1932
- (branch.attrBindings && branch.attrBindings.length > 0) ||
1933
- (branch.modelBindings && branch.modelBindings.length > 0);
1934
- if (!hasBranchSetup) continue;
1935
-
1936
- const keyword = i === 0 ? 'if' : 'else if';
1937
- lines.push(` ${keyword} (branch === ${i}) {`);
1938
-
1939
- // Bindings: generate DOM refs and effects for text bindings
1940
- for (const b of branch.bindings) {
1941
- lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
1942
- if (b.type === 'prop') {
1943
- lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
1944
- } else if (b.type === 'signal') {
1945
- const modelPropName = modelVarMap.get(b.name);
1946
- const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
1947
- lines.push(` __effect(() => { ${b.varName}.textContent = ${signalRef} ?? ''; });`);
1948
- } else if (b.type === 'computed') {
1949
- lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
1950
- } else {
1951
- // method/expression type — check for props.x pattern
1952
- let ref;
1953
- if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
1954
- const propName = b.name.slice(propsObjectName.length + 1);
1955
- ref = `this._s_${propName}()`;
1956
- } else {
1957
- ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1958
- }
1959
- lines.push(` __effect(() => { ${b.varName}.textContent = ${ref} ?? ''; });`);
1960
- }
1961
- }
1962
-
1963
- // Events: generate addEventListener
1964
- for (const e of branch.events) {
1965
- const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
1966
- lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
1967
- lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
1968
- }
1969
-
1970
- // Show bindings: generate effects
1971
- for (const sb of branch.showBindings) {
1972
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1973
- lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
1974
- lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
1975
- }
1976
-
1977
- // Attr bindings: generate effects
1978
- for (const ab of branch.attrBindings) {
1979
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1980
- lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
1981
- lines.push(` __effect(() => {`);
1982
- lines.push(` const __val = ${expr};`);
1983
- lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
1984
- lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
1985
- lines.push(` });`);
1986
- }
1987
-
1988
- // Model bindings: generate effects and listeners
1989
- for (const mb of (branch.modelBindings || [])) {
1990
- const nodeRef = pathExpr(mb.path, 'node');
1991
- lines.push(` const ${mb.varName} = ${nodeRef};`);
1992
- // Effect (signal DOM)
1993
- lines.push(` __effect(() => {`);
1994
- if (mb.prop === 'checked' && mb.radioValue !== null) {
1995
- lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
1996
- } else if (mb.prop === 'checked') {
1997
- lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
1998
- } else {
1999
- lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
2000
- }
2001
- lines.push(` });`);
2002
- // Listener (DOM signal)
2003
- if (mb.prop === 'checked' && mb.radioValue === null) {
2004
- lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
2005
- } else if (mb.coerce) {
2006
- lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
2007
- } else {
2008
- lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
2009
- }
2010
- }
2011
-
2012
- lines.push(' }');
2013
- }
2014
- lines.push(' }');
2015
- lines.push('');
2016
- }
2017
-
2018
- lines.push('}');
2019
- lines.push('');
2020
-
2021
- // ── 5. Custom element registration ──
2022
- lines.push(`if (!customElements.get('${tagName}')) customElements.define('${tagName}', ${className});`);
2023
- lines.push('');
2024
-
2025
- // ── 6. Default export (enables named imports from parent components) ──
2026
- lines.push(`export default ${className};`);
2027
-
2028
- return lines.join('\n');
2029
- }
1
+ /**
2
+ * Code Generator for wcCompiler v2.
3
+ *
4
+ * Takes a complete ParseResult (with bindings, events populated by tree-walker)
5
+ * and produces a self-contained JavaScript string with:
6
+ * - Inline mini reactive runtime (zero imports)
7
+ * - Scoped CSS injection
8
+ * - HTMLElement class with signals, computeds, effects, events
9
+ * - customElements.define registration
10
+ *
11
+ * This is a simplified version of v1's codegen, scoped to core features only:
12
+ * signals, computeds, effects, text interpolation, event bindings, user methods.
13
+ * No props, emits, slots, if, for, model, show, attr, refs, or lifecycle hooks.
14
+ */
15
+
16
+ import { reactiveRuntime, buildInlineRuntime } from './reactive-runtime.js';
17
+ import { scopeCSS } from './css-scoper.js';
18
+ import { camelToKebab } from './parser-extractors.js';
19
+
20
+ /** @import { ParseResult } from './types.js' */
21
+
22
+ /**
23
+ * Convert a path array to a JS expression string.
24
+ * e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
25
+ *
26
+ * @param {string[]} parts
27
+ * @param {string} rootVar
28
+ * @returns {string}
29
+ */
30
+ export function pathExpr(parts, rootVar) {
31
+ return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
32
+ }
33
+
34
+ /**
35
+ * Escape special regex characters in a string.
36
+ *
37
+ * @param {string} str
38
+ * @returns {string}
39
+ */
40
+ function escapeRegex(str) {
41
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+ }
43
+
44
+ /**
45
+ * Get the signal reference for a slot prop source expression.
46
+ *
47
+ * @param {string} source — Source variable name from :prop="source"
48
+ * @param {string[]} signalNames — Signal variable names
49
+ * @param {string[]} computedNames — Computed variable names
50
+ * @param {Set<string>} propNames — Prop names from defineProps
51
+ * @returns {string}
52
+ */
53
+ function slotPropRef(source, signalNames, computedNames, propNames) {
54
+ if (propNames.has(source)) return `this._s_${source}()`;
55
+ if (computedNames.includes(source)) return `this._c_${source}()`;
56
+ if (signalNames.includes(source)) return `this._${source}()`;
57
+ return `'${source}'`;
58
+ }
59
+
60
+ /**
61
+ * Transform an expression by rewriting signal/computed variable references
62
+ * to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
63
+ *
64
+ * Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
65
+ * Also handles `emitsObjectName(` → `this._emit(` transformation.
66
+ *
67
+ * Uses word-boundary regex for each known signal/computed name.
68
+ * Does NOT transform if the name is followed by `.set(` (that's a write,
69
+ * handled by transformMethodBody).
70
+ *
71
+ * @param {string} expr — Expression to transform
72
+ * @param {string[]} signalNames — Signal variable names
73
+ * @param {string[]} computedNames — Computed variable names
74
+ * @param {string|null} [propsObjectName] — Props object variable name
75
+ * @param {Set<string>} [propNames] — Set of prop names
76
+ * @param {string|null} [emitsObjectName] — Emits object variable name
77
+ * @returns {string}
78
+ */
79
+ export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = [], modelVarMap = new Map()) {
80
+ let result = expr;
81
+
82
+ // Transform emit calls: emitsObjectName( → this._emit(
83
+ if (emitsObjectName) {
84
+ const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
85
+ result = result.replace(emitsRe, 'this._emit(');
86
+ }
87
+
88
+ // Transform method calls: methodName( → this._methodName(
89
+ for (const name of methodNames) {
90
+ if (propsObjectName && name === propsObjectName) continue;
91
+ if (emitsObjectName && name === emitsObjectName) continue;
92
+ const methodRe = new RegExp(`\\b${name}\\(`, 'g');
93
+ result = result.replace(methodRe, `this._${name}(`);
94
+ }
95
+
96
+ // Transform props.x → this._s_x() BEFORE signal/computed transforms
97
+ if (propsObjectName && propNames.size > 0) {
98
+ const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
99
+ result = result.replace(propsRe, (match, propName) => {
100
+ if (propNames.has(propName)) {
101
+ return `this._s_${propName}()`;
102
+ }
103
+ return match; // leave unknown props unchanged
104
+ });
105
+ }
106
+
107
+ // Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
108
+ for (const propName of propNames) {
109
+ if (propsObjectName && propName === propsObjectName) continue;
110
+ if (emitsObjectName && propName === emitsObjectName) continue;
111
+ const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
112
+ result = result.replace(bareRe, `this._s_${propName}()`);
113
+ }
114
+
115
+ // Transform model signal reads: varName() → this._m_{propName}() (BEFORE regular signals)
116
+ for (const [varName, propNameVal] of modelVarMap) {
117
+ if (propsObjectName && varName === propsObjectName) continue;
118
+ if (emitsObjectName && varName === emitsObjectName) continue;
119
+ // First: transform varName() calls → this._m_propName()
120
+ const callRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
121
+ result = result.replace(callRe, `this._m_${propNameVal}()`);
122
+ // Then: transform bare varName references (not followed by ( or .set()) → this._m_propName()
123
+ const bareRe = new RegExp(`\\b(${varName})\\b(?!\\.set\\()(?!\\()`, 'g');
124
+ result = result.replace(bareRe, `this._m_${propNameVal}()`);
125
+ }
126
+
127
+ // Transform computed names first (to avoid partial matches with signals)
128
+ for (const name of computedNames) {
129
+ // Skip propsObjectName and emitsObjectName
130
+ if (propsObjectName && name === propsObjectName) continue;
131
+ if (emitsObjectName && name === emitsObjectName) continue;
132
+ // First: transform name() calls → this._c_name() (replace the call, not just the name)
133
+ const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
134
+ result = result.replace(callRe, `this._c_${name}()`);
135
+ // Then: transform bare name references (not followed by ( or .set() → this._c_name()
136
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
137
+ result = result.replace(bareRe, `this._c_${name}()`);
138
+ }
139
+
140
+ // Transform signal names
141
+ for (const name of signalNames) {
142
+ // Skip propsObjectName and emitsObjectName
143
+ if (propsObjectName && name === propsObjectName) continue;
144
+ if (emitsObjectName && name === emitsObjectName) continue;
145
+ // First: transform name() calls → this._name() (replace the call, not just the name)
146
+ const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
147
+ result = result.replace(callRe, `this._${name}()`);
148
+ // Then: transform bare name references (not followed by ( or .set() → this._name()
149
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
150
+ result = result.replace(bareRe, `this._${name}()`);
151
+ }
152
+
153
+ // Transform constant names → this._const_name (no function call)
154
+ for (const name of constantNames) {
155
+ if (propsObjectName && name === propsObjectName) continue;
156
+ if (emitsObjectName && name === emitsObjectName) continue;
157
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
158
+ result = result.replace(bareRe, `this._const_${name}`);
159
+ }
160
+
161
+ return result;
162
+ }
163
+
164
+ /**
165
+ * Transform a method/effect body by rewriting signal writes and reads.
166
+ *
167
+ * - `emitsObjectName(` → `this._emit(` (emit call)
168
+ * - `props.x` → `this._s_x()` (prop access)
169
+ * - `varName.set(value)` → `this._modelSet_{propName}(value)` (model signal write)
170
+ * - `x.set(value)` → `this._x(value)` (signal write via setter)
171
+ * - `varName()` → `this._m_{propName}()` (model signal read)
172
+ * - `x()` → `this._x()` (signal read)
173
+ * - Computed `x()` → `this._c_x()` (computed read)
174
+ *
175
+ * @param {string} body — Function body to transform
176
+ * @param {string[]} signalNames — Signal variable names
177
+ * @param {string[]} computedNames — Computed variable names
178
+ * @param {string|null} [propsObjectName] — Props object variable name
179
+ * @param {Set<string>} [propNames] — Set of prop names
180
+ * @param {string|null} [emitsObjectName] — Emits object variable name
181
+ * @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
182
+ * @param {string[]} [constantNames] — Constant variable names
183
+ * @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
184
+ * @returns {string}
185
+ */
186
+ export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = [], modelVarMap = new Map()) {
187
+ let result = body;
188
+
189
+ // 0a. Transform emit calls: emitsObjectName( → this._emit(
190
+ if (emitsObjectName) {
191
+ const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
192
+ result = result.replace(emitsRe, 'this._emit(');
193
+ }
194
+
195
+ // 0b. Transform props.x → this._s_x() BEFORE other transforms
196
+ if (propsObjectName && propNames.size > 0) {
197
+ const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
198
+ result = result.replace(propsRe, (match, propName) => {
199
+ if (propNames.has(propName)) {
200
+ return `this._s_${propName}()`;
201
+ }
202
+ return match;
203
+ });
204
+ }
205
+
206
+ // 0c. Transform ref access: varName.value → this._varName.value
207
+ for (const name of refVarNames) {
208
+ const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
209
+ result = result.replace(refRe, `this._${name}.value`);
210
+ }
211
+
212
+ // 0d. Transform model signal writes: varName.set(expr) → this._modelSet_{propName}(expr)
213
+ // Must run BEFORE regular signal .set() transforms
214
+ for (const [varName, propNameVal] of modelVarMap) {
215
+ if (propsObjectName && varName === propsObjectName) continue;
216
+ if (emitsObjectName && varName === emitsObjectName) continue;
217
+ const setRe = new RegExp(`\\b${varName}\\.set\\(`, 'g');
218
+ result = result.replace(setRe, `this._modelSet_${propNameVal}(`);
219
+ }
220
+
221
+ // 1. Transform signal writes: x.set(value) → this._x(value)
222
+ for (const name of signalNames) {
223
+ if (propsObjectName && name === propsObjectName) continue;
224
+ if (emitsObjectName && name === emitsObjectName) continue;
225
+ const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
226
+ result = result.replace(setRe, `this._${name}(`);
227
+ }
228
+
229
+ // 1b. Transform model signal reads: varName() → this._m_{propName}()
230
+ // Must run BEFORE regular signal read transforms
231
+ for (const [varName, propNameVal] of modelVarMap) {
232
+ if (propsObjectName && varName === propsObjectName) continue;
233
+ if (emitsObjectName && varName === emitsObjectName) continue;
234
+ const readRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
235
+ result = result.replace(readRe, `this._m_${propNameVal}()`);
236
+ }
237
+
238
+ // 2. Transform computed reads: x() → this._c_x()
239
+ for (const name of computedNames) {
240
+ if (propsObjectName && name === propsObjectName) continue;
241
+ if (emitsObjectName && name === emitsObjectName) continue;
242
+ const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
243
+ result = result.replace(readRe, `this._c_${name}()`);
244
+ }
245
+
246
+ // 3. Transform signal reads: x() → this._x()
247
+ for (const name of signalNames) {
248
+ if (propsObjectName && name === propsObjectName) continue;
249
+ if (emitsObjectName && name === emitsObjectName) continue;
250
+ const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
251
+ result = result.replace(readRe, `this._${name}()`);
252
+ }
253
+
254
+ // 4. Transform constant reads: name → this._const_name
255
+ for (const name of constantNames) {
256
+ if (propsObjectName && name === propsObjectName) continue;
257
+ if (emitsObjectName && name === emitsObjectName) continue;
258
+ const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
259
+ result = result.replace(bareRe, `this._const_${name}`);
260
+ }
261
+
262
+ return result;
263
+ }
264
+
265
+ /**
266
+ * Transform an expression within the scope of an each block.
267
+ * - References to itemVar and indexVar are left UNTRANSFORMED
268
+ * - References to component variables (props, reactive, computed) ARE transformed
269
+ *
270
+ * @param {string} expr - The expression to transform
271
+ * @param {string} itemVar - Name of the iteration variable
272
+ * @param {string | null} indexVar - Name of the index variable
273
+ * @param {Set<string>} propsSet
274
+ * @param {Set<string>} rootVarNames - Set of signal names
275
+ * @param {Set<string>} computedNames
276
+ * @returns {string}
277
+ */
278
+ export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
279
+ let r = expr;
280
+ const excludeSet = new Set([itemVar]);
281
+ if (indexVar) excludeSet.add(indexVar);
282
+
283
+ for (const p of propsSet) {
284
+ if (excludeSet.has(p)) continue;
285
+ // First: transform name() calls → this._s_name() (don't double-call)
286
+ r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
287
+ // Then: transform bare name references
288
+ r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
289
+ }
290
+ for (const n of rootVarNames) {
291
+ if (excludeSet.has(n)) continue;
292
+ // First: transform name() calls → this._name() (don't double-call)
293
+ r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
294
+ // Then: transform bare name references
295
+ r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
296
+ }
297
+ for (const n of computedNames) {
298
+ if (excludeSet.has(n)) continue;
299
+ // First: transform name() calls → this._c_name() (don't double-call)
300
+ r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
301
+ // Then: transform bare name references
302
+ r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
303
+ }
304
+ return r;
305
+ }
306
+
307
+ /**
308
+ * Check if a binding name is static within an each scope (references only item/index).
309
+ * A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
310
+ *
311
+ * @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
312
+ * @param {string} itemVar
313
+ * @param {string | null} indexVar
314
+ * @returns {boolean}
315
+ */
316
+ export function isStaticForBinding(name, itemVar, indexVar) {
317
+ if (name === itemVar || name.startsWith(itemVar + '.')) return true;
318
+ if (indexVar && name === indexVar) return true;
319
+ return false;
320
+ }
321
+
322
+ /**
323
+ * Check if an expression is static within an each scope (references only item/index, no component vars).
324
+ *
325
+ * @param {string} expr
326
+ * @param {string} itemVar
327
+ * @param {string | null} indexVar
328
+ * @param {Set<string>} propsSet
329
+ * @param {Set<string>} rootVarNames
330
+ * @param {Set<string>} computedNames
331
+ * @returns {boolean}
332
+ */
333
+ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
334
+ const excludeSet = new Set([itemVar]);
335
+ if (indexVar) excludeSet.add(indexVar);
336
+
337
+ for (const p of propsSet) {
338
+ if (excludeSet.has(p)) continue;
339
+ if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
340
+ }
341
+ for (const n of rootVarNames) {
342
+ if (excludeSet.has(n)) continue;
343
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
344
+ }
345
+ for (const n of computedNames) {
346
+ if (excludeSet.has(n)) continue;
347
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
348
+ }
349
+ return true;
350
+ }
351
+
352
+ /**
353
+ * Generate the JS expression for an event handler based on its type:
354
+ * - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
355
+ * - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
356
+ * - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
357
+ *
358
+ * @param {string} handler — The raw handler string from the template
359
+ * @param {string[]} signalNames
360
+ * @param {string[]} computedNames
361
+ * @param {string|null} propsObjectName
362
+ * @param {Set<string>} propNames
363
+ * @param {string|null} emitsObjectName
364
+ * @param {string[]} constantNames
365
+ * @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
366
+ * @returns {string}
367
+ */
368
+ export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap = new Map()) {
369
+ if (handler.includes('=>')) {
370
+ // Arrow function expression: (e) => removeItem(item)
371
+ const arrowIdx = handler.indexOf('=>');
372
+ const params = handler.slice(0, arrowIdx).trim();
373
+ let body = handler.slice(arrowIdx + 2).trim();
374
+ body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames, modelVarMap);
375
+ return `${params} => { ${body}; }`;
376
+ } else if (handler.includes('(')) {
377
+ // Function call expression: removeItem(item)
378
+ const parenIdx = handler.indexOf('(');
379
+ const fnName = handler.slice(0, parenIdx).trim();
380
+ const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
381
+ const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap) : '';
382
+ return `(e) => { this._${fnName}(${transformedArgs}); }`;
383
+ } else {
384
+ // Simple method name
385
+ return `this._${handler}.bind(this)`;
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Generate the JS expression for an event handler inside an each block.
391
+ * Similar to generateEventHandler but uses transformForExpr for the each scope.
392
+ *
393
+ * @param {string} handler
394
+ * @param {string} itemVar
395
+ * @param {string|null} indexVar
396
+ * @param {Set<string>} propNames
397
+ * @param {Set<string>} signalNamesSet
398
+ * @param {Set<string>} computedNamesSet
399
+ * @returns {string}
400
+ */
401
+ export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
402
+ if (handler.includes('=>')) {
403
+ // Arrow function expression
404
+ const arrowIdx = handler.indexOf('=>');
405
+ const params = handler.slice(0, arrowIdx).trim();
406
+ let body = handler.slice(arrowIdx + 2).trim();
407
+ body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
408
+ return `${params} => { ${body}; }`;
409
+ } else if (handler.includes('(')) {
410
+ // Function call expression: removeItem(item)
411
+ const parenIdx = handler.indexOf('(');
412
+ const fnName = handler.slice(0, parenIdx).trim();
413
+ const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
414
+ const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
415
+ return `(e) => { this._${fnName}(${transformedArgs}); }`;
416
+ } else {
417
+ // Simple method name
418
+ return `this._${handler}.bind(this)`;
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Generate per-item setup code for bindings, events, show, attr, model, and slots.
424
+ * Used by both keyed and non-keyed each effects.
425
+ *
426
+ * @param {string[]} lines — Output lines array
427
+ * @param {object} forBlock — ForBlock with bindings, events, etc.
428
+ * @param {string} itemVar
429
+ * @param {string|null} indexVar
430
+ * @param {Set<string>} propNames
431
+ * @param {Set<string>} signalNamesSet
432
+ * @param {Set<string>} computedNamesSet
433
+ */
434
+ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
435
+ const indent = ' ';
436
+
437
+ // Bindings
438
+ for (const b of forBlock.bindings) {
439
+ const nodeRef = pathExpr(b.path, 'node');
440
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
441
+ lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
442
+ } else {
443
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
444
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
445
+ }
446
+ }
447
+
448
+ // Events
449
+ for (const e of forBlock.events) {
450
+ const nodeRef = pathExpr(e.path, 'node');
451
+ const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
452
+ lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
453
+ }
454
+
455
+ // Show
456
+ for (const sb of forBlock.showBindings) {
457
+ const nodeRef = pathExpr(sb.path, 'node');
458
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
459
+ lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
460
+ } else {
461
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
462
+ lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
463
+ }
464
+ }
465
+
466
+ // Attr bindings
467
+ for (const ab of forBlock.attrBindings) {
468
+ const nodeRef = pathExpr(ab.path, 'node');
469
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
470
+ lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
471
+ lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
472
+ } else {
473
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
474
+ lines.push(`${indent} __effect(() => {`);
475
+ lines.push(`${indent} const __val = ${expr};`);
476
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
477
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
478
+ lines.push(`${indent} });`);
479
+ }
480
+ }
481
+
482
+ // Model bindings
483
+ for (const mb of (forBlock.modelBindings || [])) {
484
+ const nodeRef = pathExpr(mb.path, 'node');
485
+ lines.push(`${indent} __effect(() => {`);
486
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
487
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
488
+ } else if (mb.prop === 'checked') {
489
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
490
+ } else {
491
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
492
+ }
493
+ lines.push(`${indent} });`);
494
+ if (mb.prop === 'checked' && mb.radioValue === null) {
495
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
496
+ } else if (mb.coerce) {
497
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
498
+ } else {
499
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
500
+ }
501
+ }
502
+
503
+ // Scoped slots
504
+ for (const s of (forBlock.slots || [])) {
505
+ if (s.slotProps.length > 0) {
506
+ const slotNodeRef = pathExpr(s.path, 'node');
507
+ const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
508
+ lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
509
+ lines.push(`${indent} const __sp = { ${propsEntries} };`);
510
+ lines.push(`${indent} let __h = __slotEl.innerHTML;`);
511
+ lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
512
+ lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '(\\\\(\\\\))?\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
513
+ lines.push(`${indent} }`);
514
+ lines.push(`${indent} __slotEl.innerHTML = __h;`);
515
+ lines.push(`${indent} }`);
516
+ }
517
+ }
518
+
519
+ // Nested each directives (forBlocks)
520
+ for (const innerFor of (forBlock.forBlocks || [])) {
521
+ const innerVn = innerFor.varName;
522
+ const innerItemVar = innerFor.itemVar;
523
+ const innerIndexVar = innerFor.indexVar;
524
+ const innerSource = innerFor.source;
525
+ const innerKeyExpr = innerFor.keyExpr;
526
+
527
+ // Build excludeSet that includes BOTH outer and inner loop variables
528
+ // so transformForExpr does not rewrite them as signals
529
+ const outerExcludeVars = [itemVar];
530
+ if (indexVar) outerExcludeVars.push(indexVar);
531
+ const innerExcludeVars = [innerItemVar];
532
+ if (innerIndexVar) innerExcludeVars.push(innerIndexVar);
533
+
534
+ // Create inner template element
535
+ lines.push(`${indent} const ${innerVn}_tpl = document.createElement('template');`);
536
+ lines.push(`${indent} ${innerVn}_tpl.innerHTML = \`${innerFor.templateHtml}\`;`);
537
+
538
+ // Find inner anchor comment in the cloned outer item node
539
+ lines.push(`${indent} const ${innerVn}_anchor = ${pathExpr(innerFor.anchorPath, 'node')};`);
540
+
541
+ // Transform the inner source expression (may reference outer item var)
542
+ const innerSourceExpr = transformForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
543
+
544
+ // Determine if inner source is static (only references outer loop vars)
545
+ const innerSourceIsStatic = isStaticForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
546
+
547
+ if (innerKeyExpr) {
548
+ // ── Keyed reconciliation for nested each ──
549
+ lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
550
+ lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
551
+ lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
552
+ lines.push(`${indent} : (${innerVn}_source || []);`);
553
+ lines.push(`${indent} const ${innerVn}_newNodes = [];`);
554
+ lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
555
+ lines.push(`${indent} const __key = ${innerKeyExpr};`);
556
+ lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
557
+ lines.push(`${indent} const innerNode = clone.firstChild;`);
558
+
559
+ // Generate inner item bindings with combined excludeSet
560
+ generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
561
+
562
+ lines.push(`${indent} ${innerVn}_newNodes.push(innerNode);`);
563
+ lines.push(`${indent} });`);
564
+ lines.push(`${indent} for (const n of ${innerVn}_newNodes) { ${innerVn}_anchor.parentNode.insertBefore(n, ${innerVn}_anchor); }`);
565
+ } else {
566
+ // ── Non-keyed nested each: iterate and clone ──
567
+ lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
568
+ lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
569
+ lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
570
+ lines.push(`${indent} : (${innerVn}_source || []);`);
571
+ lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
572
+ lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
573
+ lines.push(`${indent} const innerNode = clone.firstChild;`);
574
+
575
+ // Generate inner item bindings with combined excludeSet
576
+ generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
577
+
578
+ lines.push(`${indent} ${innerVn}_anchor.parentNode.insertBefore(innerNode, ${innerVn}_anchor);`);
579
+ lines.push(`${indent} });`);
580
+ }
581
+ }
582
+
583
+ // Nested if/else-if/else chains (ifBlocks)
584
+ for (const ifBlock of (forBlock.ifBlocks || [])) {
585
+ const vn = ifBlock.varName;
586
+ const branches = ifBlock.branches;
587
+
588
+ // 3.1: Create template elements for each branch
589
+ for (let i = 0; i < branches.length; i++) {
590
+ const branch = branches[i];
591
+ lines.push(`${indent} const ${vn}_t${i} = document.createElement('template');`);
592
+ lines.push(`${indent} ${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
593
+ }
594
+
595
+ // 3.1: Find anchor comment in the cloned node
596
+ lines.push(`${indent} const ${vn}_anchor = ${pathExpr(ifBlock.anchorPath, 'node')};`);
597
+
598
+ // 3.2: Generate per-item conditional evaluation (static, not reactive)
599
+ lines.push(`${indent} let ${vn}_branch = null;`);
600
+ for (let i = 0; i < branches.length; i++) {
601
+ const branch = branches[i];
602
+ if (branch.type === 'if') {
603
+ lines.push(`${indent} if (${branch.expression}) { ${vn}_branch = ${i}; }`);
604
+ } else if (branch.type === 'else-if') {
605
+ lines.push(`${indent} else if (${branch.expression}) { ${vn}_branch = ${i}; }`);
606
+ } else {
607
+ // else
608
+ lines.push(`${indent} else { ${vn}_branch = ${i}; }`);
609
+ }
610
+ }
611
+
612
+ // 3.3: Insert only the matching branch node and apply branch bindings/events/show/attr/model
613
+ lines.push(`${indent} if (${vn}_branch !== null) {`);
614
+ const tplArray = branches.map((_, i) => `${vn}_t${i}`).join(', ');
615
+ lines.push(`${indent} const ${vn}_tpl = [${tplArray}][${vn}_branch];`);
616
+ lines.push(`${indent} const ${vn}_clone = ${vn}_tpl.content.cloneNode(true);`);
617
+ lines.push(`${indent} const ${vn}_node = ${vn}_clone.firstChild;`);
618
+ lines.push(`${indent} ${vn}_anchor.parentNode.insertBefore(${vn}_node, ${vn}_anchor);`);
619
+
620
+ // Apply branch bindings/events/show/attr/model using the outer loop's item variable
621
+ const hasSetup = branches.some(b =>
622
+ (b.bindings && b.bindings.length > 0) ||
623
+ (b.events && b.events.length > 0) ||
624
+ (b.showBindings && b.showBindings.length > 0) ||
625
+ (b.attrBindings && b.attrBindings.length > 0) ||
626
+ (b.modelBindings && b.modelBindings.length > 0)
627
+ );
628
+ if (hasSetup) {
629
+ // Generate per-branch setup inline (static evaluation using item variable)
630
+ for (let i = 0; i < branches.length; i++) {
631
+ const branch = branches[i];
632
+ const hasBranchSetup =
633
+ (branch.bindings && branch.bindings.length > 0) ||
634
+ (branch.events && branch.events.length > 0) ||
635
+ (branch.showBindings && branch.showBindings.length > 0) ||
636
+ (branch.attrBindings && branch.attrBindings.length > 0) ||
637
+ (branch.modelBindings && branch.modelBindings.length > 0);
638
+ if (!hasBranchSetup) continue;
639
+
640
+ const keyword = i === 0 ? 'if' : 'else if';
641
+ lines.push(`${indent} ${keyword} (${vn}_branch === ${i}) {`);
642
+
643
+ // Bindings (static: use item var directly)
644
+ for (const b of branch.bindings) {
645
+ const nodeRef = pathExpr(b.path, `${vn}_node`);
646
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
647
+ lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
648
+ } else {
649
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
650
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
651
+ }
652
+ }
653
+
654
+ // Events
655
+ for (const e of branch.events) {
656
+ const nodeRef = pathExpr(e.path, `${vn}_node`);
657
+ const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
658
+ lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
659
+ }
660
+
661
+ // Show bindings
662
+ for (const sb of (branch.showBindings || [])) {
663
+ const nodeRef = pathExpr(sb.path, `${vn}_node`);
664
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
665
+ lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
666
+ } else {
667
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
668
+ lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
669
+ }
670
+ }
671
+
672
+ // Attr bindings
673
+ for (const ab of (branch.attrBindings || [])) {
674
+ const nodeRef = pathExpr(ab.path, `${vn}_node`);
675
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
676
+ lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
677
+ lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
678
+ } else {
679
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
680
+ lines.push(`${indent} __effect(() => {`);
681
+ lines.push(`${indent} const __val = ${expr};`);
682
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
683
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
684
+ lines.push(`${indent} });`);
685
+ }
686
+ }
687
+
688
+ // Model bindings
689
+ for (const mb of (branch.modelBindings || [])) {
690
+ const nodeRef = pathExpr(mb.path, `${vn}_node`);
691
+ lines.push(`${indent} __effect(() => {`);
692
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
693
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
694
+ } else if (mb.prop === 'checked') {
695
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
696
+ } else {
697
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
698
+ }
699
+ lines.push(`${indent} });`);
700
+ if (mb.prop === 'checked' && mb.radioValue === null) {
701
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
702
+ } else if (mb.coerce) {
703
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
704
+ } else {
705
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
706
+ }
707
+ }
708
+
709
+ lines.push(`${indent} }`);
710
+ }
711
+ }
712
+ lines.push(`${indent} }`);
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Generate inner item bindings/events/show/attr/model for a nested each directive.
718
+ * Uses transformForExpr with an excludeSet that includes BOTH outer and inner loop variables.
719
+ *
720
+ * @param {string[]} lines - Output lines array
721
+ * @param {ForBlock} innerFor - The nested ForBlock
722
+ * @param {string} outerItemVar - Outer loop item variable
723
+ * @param {string|null} outerIndexVar - Outer loop index variable
724
+ * @param {string} innerItemVar - Inner loop item variable
725
+ * @param {string|null} innerIndexVar - Inner loop index variable
726
+ * @param {Set<string>} propNames - Prop names set
727
+ * @param {Set<string>} signalNamesSet - Signal names set
728
+ * @param {Set<string>} computedNamesSet - Computed names set
729
+ * @param {string} indent - Current indentation
730
+ */
731
+ function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent) {
732
+ // Build combined exclude set with both outer and inner loop variables
733
+ const combinedExcludeItemVar = innerItemVar;
734
+ const combinedExcludeIndexVar = innerIndexVar;
735
+
736
+ // For transformForExpr, we need to ensure both outer and inner vars are excluded.
737
+ // We create a modified propNames/signalNamesSet/computedNamesSet that doesn't include
738
+ // any of the loop variables. transformForExpr already excludes itemVar/indexVar,
739
+ // but we also need to exclude the outer loop variables.
740
+ // Strategy: filter out outer loop vars from the sets passed to transformForExpr
741
+ const filteredSignalNames = new Set([...signalNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
742
+ const filteredComputedNames = new Set([...computedNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
743
+ const filteredPropNames = new Set([...propNames].filter(n => n !== outerItemVar && n !== outerIndexVar));
744
+
745
+ // Helper: check if expression is static (only references inner/outer loop vars, no signals/computeds/props)
746
+ function isNestedStatic(expr) {
747
+ // An expression is static if it only references the loop variables (outer + inner)
748
+ const allExclude = new Set([innerItemVar, outerItemVar]);
749
+ if (innerIndexVar) allExclude.add(innerIndexVar);
750
+ if (outerIndexVar) allExclude.add(outerIndexVar);
751
+
752
+ for (const p of propNames) {
753
+ if (allExclude.has(p)) continue;
754
+ if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
755
+ }
756
+ for (const n of signalNamesSet) {
757
+ if (allExclude.has(n)) continue;
758
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
759
+ }
760
+ for (const n of computedNamesSet) {
761
+ if (allExclude.has(n)) continue;
762
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
763
+ }
764
+ return true;
765
+ }
766
+
767
+ // Helper: transform expression excluding both outer and inner loop vars
768
+ function transformNested(expr) {
769
+ return transformForExpr(expr, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
770
+ }
771
+
772
+ // Bindings
773
+ for (const b of innerFor.bindings) {
774
+ const nodeRef = pathExpr(b.path, 'innerNode');
775
+ if (isNestedStatic(b.name)) {
776
+ lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
777
+ } else {
778
+ const expr = transformNested(b.name);
779
+ lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
780
+ }
781
+ }
782
+
783
+ // Events
784
+ for (const e of innerFor.events) {
785
+ const nodeRef = pathExpr(e.path, 'innerNode');
786
+ const handlerExpr = generateForEventHandler(e.handler, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
787
+ lines.push(`${indent}${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
788
+ }
789
+
790
+ // Show
791
+ for (const sb of innerFor.showBindings) {
792
+ const nodeRef = pathExpr(sb.path, 'innerNode');
793
+ if (isNestedStatic(sb.expression)) {
794
+ lines.push(`${indent}${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
795
+ } else {
796
+ const expr = transformNested(sb.expression);
797
+ lines.push(`${indent}__effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
798
+ }
799
+ }
800
+
801
+ // Attr bindings
802
+ for (const ab of innerFor.attrBindings) {
803
+ const nodeRef = pathExpr(ab.path, 'innerNode');
804
+ if (isNestedStatic(ab.expression)) {
805
+ lines.push(`${indent}const __val_${ab.varName} = ${ab.expression};`);
806
+ lines.push(`${indent}if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
807
+ } else {
808
+ const expr = transformNested(ab.expression);
809
+ lines.push(`${indent}__effect(() => {`);
810
+ lines.push(`${indent} const __val = ${expr};`);
811
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
812
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
813
+ lines.push(`${indent}});`);
814
+ }
815
+ }
816
+
817
+ // Model bindings
818
+ for (const mb of (innerFor.modelBindings || [])) {
819
+ const nodeRef = pathExpr(mb.path, 'innerNode');
820
+ lines.push(`${indent}__effect(() => {`);
821
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
822
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
823
+ } else if (mb.prop === 'checked') {
824
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
825
+ } else {
826
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
827
+ }
828
+ lines.push(`${indent}});`);
829
+ if (mb.prop === 'checked' && mb.radioValue === null) {
830
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
831
+ } else if (mb.coerce) {
832
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
833
+ } else {
834
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
835
+ }
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Generate a fully self-contained JS component from a ParseResult.
841
+ *
842
+ * @param {ParseResult} parseResult — Complete IR with bindings/events
843
+ * @param {{ runtimeImportPath?: string }} [options] — Optional generation options
844
+ * @returns {string} JavaScript source code
845
+ */
846
+ export function generateComponent(parseResult, options = {}) {
847
+ const {
848
+ tagName,
849
+ className,
850
+ style,
851
+ signals,
852
+ computeds,
853
+ effects,
854
+ methods,
855
+ bindings,
856
+ events,
857
+ processedTemplate,
858
+ propDefs = [],
859
+ propsObjectName = null,
860
+ emits = [],
861
+ emitsObjectName = null,
862
+ ifBlocks = [],
863
+ showBindings = [],
864
+ forBlocks = [],
865
+ onMountHooks = [],
866
+ onDestroyHooks = [],
867
+ onAdoptHooks = [],
868
+ modelBindings = [],
869
+ modelPropBindings = [],
870
+ attrBindings = [],
871
+ slots = [],
872
+ constantVars = [],
873
+ watchers = [],
874
+ refs = [],
875
+ refBindings = [],
876
+ childComponents = [],
877
+ childImports = [],
878
+ exposeNames = [],
879
+ modelDefs = [],
880
+ dynamicComponents = [],
881
+ } = parseResult;
882
+
883
+ const signalNames = signals.map(s => s.name);
884
+ const computedNames = computeds.map(c => c.name);
885
+ const constantNames = constantVars.map(v => v.name);
886
+ const methodNames = methods.map(m => m.name);
887
+ const refVarNames = refs.map(r => r.varName);
888
+ const propNames = new Set(propDefs.map(p => p.name));
889
+
890
+ // Build model var name → prop name map for transform functions
891
+ const modelVarMap = new Map();
892
+ for (const md of modelDefs) {
893
+ modelVarMap.set(md.varName, md.name);
894
+ }
895
+
896
+ const lines = [];
897
+ const comment = options.comments ? (text) => lines.push(` // --- ${text} ---`) : () => {};
898
+
899
+ // ── 0. Source comment ──
900
+ if (options.sourceFile) {
901
+ lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
902
+ }
903
+
904
+ // ── 1. Reactive runtime (shared import or inline) ──
905
+ if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
906
+ // Determine which runtime functions this component needs
907
+ const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || dynamicComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
908
+ const needsComputed = computeds.length > 0;
909
+ const needsUntrack = watchers.length > 0;
910
+
911
+ if (options.runtimeImportPath) {
912
+ // Tree-shake: only import what this component actually uses
913
+ const usedRuntime = new Set(['__signal']); // always need __signal
914
+ if (needsComputed) usedRuntime.add('__computed');
915
+ if (needsEffect) usedRuntime.add('__effect');
916
+ if (needsUntrack) usedRuntime.add('__untrack');
917
+ const imports = [...usedRuntime].join(', ');
918
+ lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
919
+ } else {
920
+ // Standalone: inline only the runtime functions this component needs
921
+ lines.push(buildInlineRuntime({ needsComputed, needsEffect, needsBatch: false, needsUntrack }).trim());
922
+ }
923
+ lines.push('');
924
+
925
+ // ── 1b. Child component imports ──
926
+ for (const ci of childImports) {
927
+ if (ci.sideEffect) {
928
+ // Side-effect import: no identifier, child self-registers
929
+ lines.push(`import '${ci.importPath}';`);
930
+ } else {
931
+ // Named import with guarded registration
932
+ lines.push(`import ${ci.identifier} from '${ci.importPath}';`);
933
+ lines.push(`if (!customElements.get(${ci.identifier}.__meta.tag)) customElements.define(${ci.identifier}.__meta.tag, ${ci.identifier});`);
934
+ }
935
+ }
936
+ if (childImports.length > 0) {
937
+ lines.push('');
938
+ }
939
+
940
+ // ── 2. CSS injection (scoped, deduplicated via id guard) ──
941
+ if (style) {
942
+ if (options.comments) lines.push('// ── Styles ───────────────────────────────────────────');
943
+ const scoped = scopeCSS(style, tagName);
944
+ const cssId = `__css_${className}`;
945
+ lines.push(`if (!document.getElementById('${cssId}')) {`);
946
+ lines.push(` const ${cssId} = document.createElement('style');`);
947
+ lines.push(` ${cssId}.id = '${cssId}';`);
948
+ lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
949
+ lines.push(` document.head.appendChild(${cssId});`);
950
+ lines.push('}');
951
+ lines.push('');
952
+ }
953
+
954
+ // ── 3. Template element ──
955
+ if (options.comments) lines.push('// ── Template ─────────────────────────────────────────');
956
+ lines.push(`const __t_${className} = document.createElement('template');`);
957
+ lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
958
+ lines.push('');
959
+
960
+ // ── 4. HTMLElement class ──
961
+ if (options.comments) lines.push('// ── Component ────────────────────────────────────────');
962
+ lines.push(`class ${className} extends HTMLElement {`);
963
+
964
+ // Static observedAttributes (if props or model props exist)
965
+ const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
966
+ if (propDefs.length > 0 || modelDefs.length > 0) {
967
+ const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
968
+ // For model props, observe BOTH kebab-case AND camelCase forms
969
+ // Vue sets camelCase (modelValue), native HTML uses kebab-case (model-value)
970
+ const modelAttrEntries = [];
971
+ for (let i = 0; i < modelDefs.length; i++) {
972
+ const kebab = modelAttrNames[i];
973
+ const camel = modelDefs[i].name;
974
+ modelAttrEntries.push(`'${kebab}'`);
975
+ // Only add camelCase if it differs from kebab-case
976
+ if (kebab !== camel) {
977
+ modelAttrEntries.push(`'${camel}'`);
978
+ }
979
+ }
980
+ const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
981
+ lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
982
+ lines.push('');
983
+ }
984
+
985
+ // Static __scopedSlots array (lists slot names with reactive props)
986
+ const scopedSlotNames = slots.filter(s => s.name && s.slotProps.length > 0).map(s => s.name);
987
+ if (scopedSlotNames.length > 0) {
988
+ const scopedArr = scopedSlotNames.map(n => `'${n}'`).join(', ');
989
+ lines.push(` static __scopedSlots = [${scopedArr}];`);
990
+ lines.push('');
991
+ }
992
+
993
+ // Static __meta — component metadata for framework adapters (React wrappers, Angular events, etc.)
994
+ {
995
+ const metaProps = propDefs.map(p => `{ name: '${p.name}', default: ${p.default} }`).join(', ');
996
+ const metaEvents = emits.map(e => `'${e}'`).join(', ');
997
+ const metaModels = modelDefs.map(m => `'${m.name}'`).join(', ');
998
+ const metaSlots = slots.filter(s => s.name).map(s => `'${s.name}'`).join(', ');
999
+ lines.push(` static __meta = { tag: '${tagName}', props: [${metaProps}], events: [${metaEvents}], models: [${metaModels}], slots: [${metaSlots}] };`);
1000
+ lines.push('');
1001
+ }
1002
+
1003
+ // Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
1004
+ lines.push(' constructor() {');
1005
+ lines.push(' super();');
1006
+
1007
+ // Scoped slot storage initialization
1008
+ if (scopedSlotNames.length > 0) {
1009
+ lines.push(' this.__slotRenderers = {};');
1010
+ lines.push(' this.__slotProps = {};');
1011
+ }
1012
+
1013
+ // Prop signal initialization (BEFORE user signals)
1014
+ for (const p of propDefs) {
1015
+ lines.push(` this._s_${p.name} = __signal(${p.default});`);
1016
+ }
1017
+
1018
+ // Signal initialization
1019
+ for (const s of signals) {
1020
+ if (s === signals[0]) comment('Signals');
1021
+ lines.push(` this._${s.name} = __signal(${s.value});`);
1022
+ }
1023
+
1024
+ // Model signal initialization
1025
+ for (const md of modelDefs) {
1026
+ lines.push(` this._m_${md.name} = __signal(${md.default});`);
1027
+ }
1028
+
1029
+ // Constant initialization
1030
+ for (const c of constantVars) {
1031
+ lines.push(` this._const_${c.name} = ${c.value};`);
1032
+ }
1033
+
1034
+ // Computed initialization
1035
+ for (const c of computeds) {
1036
+ if (c === computeds[0]) comment('Computed');
1037
+ const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1038
+ lines.push(` this._c_${c.name} = __computed(() => ${body});`);
1039
+ }
1040
+
1041
+ // Watcher prev-value initialization
1042
+ for (let idx = 0; idx < watchers.length; idx++) {
1043
+ const w = watchers[idx];
1044
+ if (w.kind === 'signal') {
1045
+ // For signal watchers watching a prop, initialize with the prop's default value
1046
+ // so that attributeChangedCallback changes before connectedCallback are detected
1047
+ if (propNames.has(w.target)) {
1048
+ const propDef = propDefs.find(p => p.name === w.target);
1049
+ lines.push(` this.__prev_${w.target} = ${propDef ? propDef.default : 'undefined'};`);
1050
+ } else {
1051
+ lines.push(` this.__prev_${w.target} = undefined;`);
1052
+ }
1053
+ } else {
1054
+ // For getter watchers, check if the getter references a prop (e.g., props.value)
1055
+ // Initialize with the prop's default so pre-connection attribute changes are detected
1056
+ const propMatch = propsObjectName ? w.target.match(new RegExp(`^${propsObjectName}\\.(\\w+)$`)) : null;
1057
+ if (propMatch && propNames.has(propMatch[1])) {
1058
+ const propDef = propDefs.find(p => p.name === propMatch[1]);
1059
+ lines.push(` this.__prev_watch${idx} = ${propDef ? propDef.default : 'undefined'};`);
1060
+ } else {
1061
+ lines.push(` this.__prev_watch${idx} = undefined;`);
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ lines.push(' }');
1067
+ lines.push('');
1068
+
1069
+ // connectedCallback (idempotent — safe for re-mount)
1070
+ lines.push(' connectedCallback() {');
1071
+ lines.push(' if (this.__connected) return;');
1072
+ lines.push(' this.__connected = true;');
1073
+
1074
+ // ── DOM SETUP (moved from constructor for Custom Elements spec compliance) ──
1075
+
1076
+ // Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
1077
+ if (slots.length > 0) {
1078
+ lines.push(' const __slotMap = {};');
1079
+ lines.push(' const __defaultSlotNodes = [];');
1080
+ lines.push(' for (const child of Array.from(this.childNodes)) {');
1081
+ lines.push(" if (child.nodeName === 'TEMPLATE') {");
1082
+ lines.push(' for (const attr of child.attributes) {');
1083
+ lines.push(" if (attr.name.startsWith('#')) {");
1084
+ lines.push(' const slotName = attr.name.slice(1);');
1085
+ lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
1086
+ lines.push(' }');
1087
+ lines.push(' }');
1088
+ lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
1089
+ // NEW: regular element with slot="name" (cross-framework support)
1090
+ lines.push(" const slotName = child.getAttribute('slot');");
1091
+ lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
1092
+ lines.push(" child.removeAttribute('slot');");
1093
+ lines.push(" child.removeAttribute('slot-props');");
1094
+ lines.push(" __slotMap[slotName] = { content: propsExpr ? child.innerHTML : child.outerHTML, propsExpr };");
1095
+ lines.push(" } else if (child.nodeType === 1) {");
1096
+ // NEW: check for slot-template-<name> attributes (React/Angular string attribute pattern)
1097
+ lines.push(" for (const attr of Array.from(child.attributes)) {");
1098
+ lines.push(" if (attr.name.startsWith('slot-template-')) {");
1099
+ lines.push(" const slotName = attr.name.slice('slot-template-'.length);");
1100
+ lines.push(" if (!__slotMap[slotName]) {");
1101
+ lines.push(" __slotMap[slotName] = { content: attr.value, propsExpr: '' };");
1102
+ lines.push(" }");
1103
+ lines.push(" child.removeAttribute(attr.name);");
1104
+ lines.push(" }");
1105
+ lines.push(" }");
1106
+ lines.push(" __defaultSlotNodes.push(child);");
1107
+ lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
1108
+ lines.push(' __defaultSlotNodes.push(child);');
1109
+ lines.push(' }');
1110
+ lines.push(' }');
1111
+ }
1112
+
1113
+ // Clone template
1114
+ lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
1115
+
1116
+ // Assign DOM refs for bindings
1117
+ for (const b of bindings) {
1118
+ lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
1119
+ }
1120
+
1121
+ // Assign DOM refs for events
1122
+ for (const e of events) {
1123
+ lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
1124
+ }
1125
+
1126
+ // Assign DOM refs for show bindings
1127
+ for (const sb of showBindings) {
1128
+ lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
1129
+ }
1130
+
1131
+ // Assign DOM refs for model bindings
1132
+ for (const mb of modelBindings) {
1133
+ lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
1134
+ }
1135
+
1136
+ // Assign DOM refs for model:propName bindings
1137
+ for (const mpb of modelPropBindings) {
1138
+ lines.push(` this.${mpb.varName} = ${pathExpr(mpb.path, '__root')};`);
1139
+ }
1140
+
1141
+ // Assign DOM refs for slot placeholders
1142
+ for (const s of slots) {
1143
+ lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
1144
+ }
1145
+
1146
+ // Assign DOM refs for child component instances (only if they have prop bindings)
1147
+ for (const cc of childComponents) {
1148
+ if (cc.propBindings.length > 0) {
1149
+ lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
1150
+ }
1151
+ }
1152
+
1153
+ // Assign DOM refs for attr bindings (reuse ref when same path)
1154
+ const attrPathMap = new Map();
1155
+ for (const ab of attrBindings) {
1156
+ const pathKey = ab.path.join('.');
1157
+ if (attrPathMap.has(pathKey)) {
1158
+ lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
1159
+ } else {
1160
+ lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
1161
+ attrPathMap.set(pathKey, ab.varName);
1162
+ }
1163
+ }
1164
+
1165
+ // ── if: template creation, anchor reference, state init ──
1166
+ for (const ifBlock of ifBlocks) {
1167
+ const vn = ifBlock.varName;
1168
+ // Template per branch
1169
+ for (let i = 0; i < ifBlock.branches.length; i++) {
1170
+ const branch = ifBlock.branches[i];
1171
+ lines.push(` this.${vn}_t${i} = document.createElement('template');`);
1172
+ lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
1173
+ }
1174
+ // Reference to anchor comment node (must be before appendChild moves nodes out of __root)
1175
+ lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
1176
+ // Active branch state
1177
+ lines.push(` this.${vn}_current = null;`);
1178
+ lines.push(` this.${vn}_active = undefined;`);
1179
+ }
1180
+
1181
+ // ── each: template creation, anchor reference, nodes array ──
1182
+ for (const forBlock of forBlocks) {
1183
+ const vn = forBlock.varName;
1184
+ lines.push(` this.${vn}_tpl = document.createElement('template');`);
1185
+ lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
1186
+ lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
1187
+ lines.push(` this.${vn}_nodes = [];`);
1188
+ }
1189
+
1190
+ // ── dynamic component: anchor reference, state init ──
1191
+ for (const dyn of dynamicComponents) {
1192
+ const vn = dyn.varName;
1193
+ lines.push(` this.${vn}_anchor = ${pathExpr(dyn.anchorPath, '__root')};`);
1194
+ lines.push(` this.${vn}_current = null;`);
1195
+ lines.push(` this.${vn}_tag = null;`);
1196
+ lines.push(` this.${vn}_propDisposers = [];`);
1197
+ }
1198
+
1199
+ // ── Ref DOM reference assignments (before appendChild moves nodes) ──
1200
+ for (const rb of refBindings) {
1201
+ lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
1202
+ }
1203
+
1204
+ // Append DOM (always light DOM)
1205
+ lines.push(" this.innerHTML = '';");
1206
+ lines.push(' this.appendChild(__root);');
1207
+
1208
+ // Static slot injection (after DOM is appended)
1209
+ for (const s of slots) {
1210
+ if (s.name && s.slotProps.length > 0) {
1211
+ // Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
1212
+ lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
1213
+ if (s.defaultContent) {
1214
+ lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
1215
+ }
1216
+ } else if (s.name) {
1217
+ // Named slot: inject content directly
1218
+ lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
1219
+ } else {
1220
+ // Default slot: inject collected child nodes
1221
+ lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
1222
+ }
1223
+ }
1224
+
1225
+ // ── Deferred slot re-check (Angular compatibility) ──
1226
+ // Angular connects custom elements to DOM BEFORE projecting children.
1227
+ // If no slot content was found on first pass, schedule a microtask retry.
1228
+ // We save a reference to the rendered root node so the microtask can filter it out
1229
+ // and only process children that were projected by the framework after connectedCallback.
1230
+ if (slots.length > 0) {
1231
+ lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
1232
+ lines.push(' const __renderedRoot = this.firstElementChild;');
1233
+ lines.push(' queueMicrotask(() => {');
1234
+ lines.push(' const __sm = {};');
1235
+ lines.push(' const __dn = [];');
1236
+ lines.push(' for (const child of Array.from(this.childNodes)) {');
1237
+ // Skip the rendered template root and any whitespace text nodes that were there before
1238
+ lines.push(' if (child === __renderedRoot) continue;');
1239
+ lines.push(' if (child.nodeType === 3 && !child.textContent.trim()) continue;');
1240
+ lines.push(" if (child.nodeName === 'TEMPLATE') {");
1241
+ lines.push(' for (const attr of child.attributes) {');
1242
+ lines.push(" if (attr.name.startsWith('#')) {");
1243
+ lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
1244
+ lines.push(' }');
1245
+ lines.push(' }');
1246
+ lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
1247
+ lines.push(" const sn = child.getAttribute('slot');");
1248
+ lines.push(" const pe = child.getAttribute('slot-props') || '';");
1249
+ lines.push(" child.removeAttribute('slot');");
1250
+ lines.push(" child.removeAttribute('slot-props');");
1251
+ lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
1252
+ lines.push(" child.remove();");
1253
+ lines.push(" } else if (child.nodeType === 1) {");
1254
+ lines.push(" for (const attr of Array.from(child.attributes)) {");
1255
+ lines.push(" if (attr.name.startsWith('slot-template-')) {");
1256
+ lines.push(" const sn = attr.name.slice('slot-template-'.length);");
1257
+ lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
1258
+ lines.push(" child.removeAttribute(attr.name);");
1259
+ lines.push(" }");
1260
+ lines.push(" }");
1261
+ lines.push(" __dn.push(child);");
1262
+ lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
1263
+ lines.push(" __dn.push(child);");
1264
+ lines.push(' }');
1265
+ lines.push(' }');
1266
+ // Re-inject slots if we found content this time
1267
+ lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
1268
+ for (const s of slots) {
1269
+ if (s.name && s.slotProps.length > 0) {
1270
+ lines.push(` if (__sm['${s.name}']) {`);
1271
+ lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
1272
+ if (s.slotProps.length > 0 && s.slotProps[0].source) {
1273
+ lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
1274
+ }
1275
+ lines.push(` }`);
1276
+ } else if (s.name) {
1277
+ lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
1278
+ } else {
1279
+ lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
1280
+ }
1281
+ }
1282
+ lines.push(' }');
1283
+ lines.push(' });');
1284
+ lines.push(' }');
1285
+ }
1286
+
1287
+ // ── EFFECTS AND LISTENERS ──
1288
+ lines.push(' this.__ac = new AbortController();');
1289
+ lines.push(' this.__disposers = [];');
1290
+ lines.push('');
1291
+
1292
+ // Binding effects one __effect per binding
1293
+ if (bindings.length > 0) comment('Text bindings');
1294
+ for (const b of bindings) {
1295
+ if (b.type === 'prop') {
1296
+ lines.push(' this.__disposers.push(__effect(() => {');
1297
+ lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
1298
+ lines.push(' }));');
1299
+ } else if (b.type === 'signal') {
1300
+ // Check if this is a model var (needs _m_ prefix instead of _)
1301
+ const modelPropName = modelVarMap.get(b.name);
1302
+ const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
1303
+ lines.push(' this.__disposers.push(__effect(() => {');
1304
+ lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
1305
+ lines.push(' }));');
1306
+ } else if (b.type === 'computed') {
1307
+ lines.push(' this.__disposers.push(__effect(() => {');
1308
+ lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
1309
+ lines.push(' }));');
1310
+ } else {
1311
+ // method/expression type — check if it's a props.x access or a complex expression
1312
+ let ref;
1313
+ if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
1314
+ const propName = b.name.slice(propsObjectName.length + 1);
1315
+ ref = `this._s_${propName}()`;
1316
+ } else {
1317
+ // Use transformExpr for complex expressions (e.g. items().length, ternary)
1318
+ ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1319
+ }
1320
+ lines.push(' this.__disposers.push(__effect(() => {');
1321
+ lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
1322
+ lines.push(' }));');
1323
+ }
1324
+ }
1325
+
1326
+ // Scoped slot effects — reactive resolution of {{propName}} in consumer templates
1327
+ for (const s of slots) {
1328
+ if (s.name && s.slotProps.length > 0) {
1329
+ const propsObj = s.slotProps.map(sp => {
1330
+ const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
1331
+ return `${sp.prop}: ${ref}`;
1332
+ }).join(', ');
1333
+ // Scoped slot effect: always compute props and notify renderers
1334
+ // The effect runs regardless of whether a template was provided (Angular uses registerSlotRenderer)
1335
+ lines.push(' __effect(() => {');
1336
+ lines.push(` const __props = { ${propsObj} };`);
1337
+ // Store current props for late-registering renderers
1338
+ lines.push(` this.__slotProps['${s.name}'] = __props;`);
1339
+ // Emit wcc:slot-update event
1340
+ lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
1341
+ // Check for registered renderer (Angular directive)
1342
+ lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
1343
+ lines.push(` this.__slotRenderers['${s.name}'](__props);`);
1344
+ lines.push(` } else if (this.__slotTpl_${s.name}) {`);
1345
+ // Fallback: template-based token replacement (WCC-to-WCC, Vue, React)
1346
+ lines.push(` let __html = this.__slotTpl_${s.name};`);
1347
+ lines.push(" for (const [k, v] of Object.entries(__props)) {");
1348
+ lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
1349
+ lines.push(' }');
1350
+ lines.push(` this.${s.varName}.innerHTML = __html;`);
1351
+ lines.push(' }');
1352
+ lines.push(' });');
1353
+ }
1354
+ }
1355
+
1356
+ // Child component reactive prop bindings
1357
+ for (const cc of childComponents) {
1358
+ for (const pb of cc.propBindings) {
1359
+ let ref;
1360
+ if (pb.type === 'prop') {
1361
+ ref = `this._s_${pb.expr}()`;
1362
+ } else if (pb.type === 'computed') {
1363
+ ref = `this._c_${pb.expr}()`;
1364
+ } else if (pb.type === 'signal') {
1365
+ const modelPropName = modelVarMap.get(pb.expr);
1366
+ ref = modelPropName ? `this._m_${modelPropName}()` : `this._${pb.expr}()`;
1367
+ } else if (pb.type === 'constant') {
1368
+ ref = `this._const_${pb.expr}`;
1369
+ } else {
1370
+ ref = `this._${pb.expr}()`;
1371
+ }
1372
+ lines.push(' this.__disposers.push(__effect(() => {');
1373
+ lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
1374
+ lines.push(' }));');
1375
+ }
1376
+ }
1377
+
1378
+ // User effects
1379
+ for (const eff of effects) {
1380
+ const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1381
+ lines.push(' this.__disposers.push(__effect(() => {');
1382
+ // Indent each line of the body
1383
+ const bodyLines = body.split('\n');
1384
+ for (const line of bodyLines) {
1385
+ lines.push(` ${line}`);
1386
+ }
1387
+ lines.push(' }));');
1388
+ }
1389
+
1390
+ // Watcher effects
1391
+ for (let idx = 0; idx < watchers.length; idx++) {
1392
+ const w = watchers[idx];
1393
+ const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1394
+
1395
+ if (w.kind === 'signal') {
1396
+ // Determine the signal reference for the watch target
1397
+ let watchRef;
1398
+ if (propNames.has(w.target)) {
1399
+ watchRef = `this._s_${w.target}()`;
1400
+ } else if (computedNames.includes(w.target)) {
1401
+ watchRef = `this._c_${w.target}()`;
1402
+ } else {
1403
+ watchRef = `this._${w.target}()`;
1404
+ }
1405
+ lines.push(' this.__disposers.push(__effect(() => {');
1406
+ lines.push(` const ${w.newParam} = ${watchRef};`);
1407
+ lines.push(` if (this.__prev_${w.target} !== undefined && this.__prev_${w.target} !== ${w.newParam}) {`);
1408
+ if (w.oldParam) {
1409
+ lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
1410
+ }
1411
+ lines.push(' __untrack(() => {');
1412
+ const bodyLines = body.split('\n');
1413
+ for (const line of bodyLines) {
1414
+ lines.push(` ${line}`);
1415
+ }
1416
+ lines.push(' });');
1417
+ lines.push(' }');
1418
+ lines.push(` this.__prev_${w.target} = ${w.newParam};`);
1419
+ lines.push(' }));');
1420
+ } else {
1421
+ // kind === 'getter' — transform the getter expression and use it directly
1422
+ const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1423
+ const prevName = `__prev_watch${idx}`;
1424
+ lines.push(' this.__disposers.push(__effect(() => {');
1425
+ lines.push(` const ${w.newParam} = ${getterExpr};`);
1426
+ lines.push(` if (this.${prevName} !== undefined && this.${prevName} !== ${w.newParam}) {`);
1427
+ if (w.oldParam) {
1428
+ lines.push(` const ${w.oldParam} = this.${prevName};`);
1429
+ }
1430
+ lines.push(' __untrack(() => {');
1431
+ const bodyLines2 = body.split('\n');
1432
+ for (const line of bodyLines2) {
1433
+ lines.push(` ${line}`);
1434
+ }
1435
+ lines.push(' });');
1436
+ lines.push(' }');
1437
+ lines.push(` this.${prevName} = ${w.newParam};`);
1438
+ lines.push(' }));');
1439
+ }
1440
+ }
1441
+
1442
+ // Event listeners (with AbortController signal for cleanup)
1443
+ if (events.length > 0) comment('Event listeners');
1444
+ for (const e of events) {
1445
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
1446
+ lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
1447
+ }
1448
+
1449
+ // Show effects one __effect per ShowBinding
1450
+ if (showBindings.length > 0) comment('Show directives');
1451
+ for (const sb of showBindings) {
1452
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1453
+ lines.push(' this.__disposers.push(__effect(() => {');
1454
+ lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
1455
+ lines.push(' }));');
1456
+ }
1457
+
1458
+ // Model effects — signal → DOM (one __effect per ModelBinding)
1459
+ if (modelBindings.length > 0) comment('Model bindings (signal → DOM)');
1460
+ for (const mb of modelBindings) {
1461
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
1462
+ // Radio: compare signal value to radioValue
1463
+ lines.push(' this.__disposers.push(__effect(() => {');
1464
+ lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
1465
+ lines.push(' }));');
1466
+ } else if (mb.prop === 'checked') {
1467
+ // Checkbox: coerce to boolean
1468
+ lines.push(' this.__disposers.push(__effect(() => {');
1469
+ lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
1470
+ lines.push(' }));');
1471
+ } else {
1472
+ // Value-based (text, number, textarea, select): nullish coalesce to ''
1473
+ lines.push(' this.__disposers.push(__effect(() => {');
1474
+ lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
1475
+ lines.push(' }));');
1476
+ }
1477
+ }
1478
+
1479
+ // Model event listeners DOM signal (with AbortController signal)
1480
+ for (const mb of modelBindings) {
1481
+ if (mb.prop === 'checked' && mb.radioValue === null) {
1482
+ // Checkbox: read e.target.checked
1483
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); }, { signal: this.__ac.signal });`);
1484
+ } else if (mb.coerce) {
1485
+ // Number input: wrap in Number()
1486
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); }, { signal: this.__ac.signal });`);
1487
+ } else {
1488
+ // All others: read e.target.value
1489
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); }, { signal: this.__ac.signal });`);
1490
+ }
1491
+ }
1492
+
1493
+ // model:propName effects and listeners — bidirectional WCC-to-WCC binding
1494
+ for (const mpb of modelPropBindings) {
1495
+ // Determine the signal read/write expressions
1496
+ // If the signal is a model var, use this._m_propName(); otherwise use this._signalName()
1497
+ const isModelVar = modelVarMap.has(mpb.signal);
1498
+ const readExpr = isModelVar
1499
+ ? `this._m_${modelVarMap.get(mpb.signal)}()`
1500
+ : `this._${mpb.signal}()`;
1501
+ const writeExpr = isModelVar
1502
+ ? `this._m_${modelVarMap.get(mpb.signal)}`
1503
+ : `this._${mpb.signal}`;
1504
+
1505
+ // Reactive parent → child sync: set child's attribute from parent signal
1506
+ const attrName = camelToKebab(mpb.propName);
1507
+ lines.push(' this.__disposers.push(__effect(() => {');
1508
+ lines.push(` this.${mpb.varName}.setAttribute('${attrName}', ${readExpr} ?? '');`);
1509
+ lines.push(' }));');
1510
+
1511
+ // Child parent sync: listen for wcc:model on child, update parent signal
1512
+ lines.push(` this.${mpb.varName}.addEventListener('wcc:model', (e) => {`);
1513
+ lines.push(` if (e.detail.prop === '${mpb.propName}') {`);
1514
+ lines.push(` ${writeExpr}(e.detail.value);`);
1515
+ lines.push(' }');
1516
+ lines.push(' }, { signal: this.__ac.signal });');
1517
+ }
1518
+
1519
+ // Attr binding effects — one __effect per AttrBinding
1520
+ for (const ab of attrBindings) {
1521
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1522
+ if (ab.kind === 'attr') {
1523
+ lines.push(' this.__disposers.push(__effect(() => {');
1524
+ lines.push(` const __v = ${expr};`);
1525
+ lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
1526
+ lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
1527
+ lines.push(' }));');
1528
+ } else if (ab.kind === 'bool') {
1529
+ lines.push(' this.__disposers.push(__effect(() => {');
1530
+ lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
1531
+ lines.push(' }));');
1532
+ } else if (ab.kind === 'class') {
1533
+ if (ab.expression.trimStart().startsWith('{')) {
1534
+ // Object expression: iterate entries, classList.add/remove
1535
+ lines.push(' this.__disposers.push(__effect(() => {');
1536
+ lines.push(` const __obj = ${expr};`);
1537
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
1538
+ lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
1539
+ lines.push(' }');
1540
+ lines.push(' }));');
1541
+ } else {
1542
+ // String expression: set className
1543
+ lines.push(' this.__disposers.push(__effect(() => {');
1544
+ lines.push(` this.${ab.varName}.className = ${expr};`);
1545
+ lines.push(' }));');
1546
+ }
1547
+ } else if (ab.kind === 'style') {
1548
+ if (ab.expression.trimStart().startsWith('{')) {
1549
+ // Object expression: iterate entries, set style[key]
1550
+ lines.push(' this.__disposers.push(__effect(() => {');
1551
+ lines.push(` const __obj = ${expr};`);
1552
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
1553
+ lines.push(` this.${ab.varName}.style[__k] = __val;`);
1554
+ lines.push(' }');
1555
+ lines.push(' }));');
1556
+ } else {
1557
+ // String expression: set cssText
1558
+ lines.push(' this.__disposers.push(__effect(() => {');
1559
+ lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
1560
+ lines.push(' }));');
1561
+ }
1562
+ }
1563
+ }
1564
+
1565
+ // ── if effects ──
1566
+ for (const ifBlock of ifBlocks) {
1567
+ const vn = ifBlock.varName;
1568
+ lines.push(' this.__disposers.push(__effect(() => {');
1569
+ lines.push(' let __branch = null;');
1570
+ for (let i = 0; i < ifBlock.branches.length; i++) {
1571
+ const branch = ifBlock.branches[i];
1572
+ if (branch.type === 'if') {
1573
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1574
+ lines.push(` if (${expr}) { __branch = ${i}; }`);
1575
+ } else if (branch.type === 'else-if') {
1576
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1577
+ lines.push(` else if (${expr}) { __branch = ${i}; }`);
1578
+ } else {
1579
+ // else
1580
+ lines.push(` else { __branch = ${i}; }`);
1581
+ }
1582
+ }
1583
+ lines.push(` if (__branch === this.${vn}_active) return;`);
1584
+ // Remove previous branch
1585
+ lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
1586
+ // Insert new branch
1587
+ lines.push(' if (__branch !== null) {');
1588
+ const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
1589
+ lines.push(` const tpl = [${tplArray}][__branch];`);
1590
+ lines.push(' const clone = tpl.content.cloneNode(true);');
1591
+ lines.push(' const node = clone.firstChild;');
1592
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1593
+ lines.push(' customElements.upgrade(node);');
1594
+ lines.push(` this.${vn}_current = node;`);
1595
+ // Setup bindings/events for active branch (only if any branch has bindings/events)
1596
+ const hasSetup = ifBlock.branches.some(b =>
1597
+ (b.bindings && b.bindings.length > 0) ||
1598
+ (b.events && b.events.length > 0) ||
1599
+ (b.showBindings && b.showBindings.length > 0) ||
1600
+ (b.attrBindings && b.attrBindings.length > 0) ||
1601
+ (b.modelBindings && b.modelBindings.length > 0)
1602
+ );
1603
+ if (hasSetup) {
1604
+ lines.push(` this.${vn}_setup(node, __branch);`);
1605
+ }
1606
+ lines.push(' }');
1607
+ lines.push(` this.${vn}_active = __branch;`);
1608
+ lines.push(' }));');
1609
+ }
1610
+
1611
+ // ── each effects ──
1612
+ for (const forBlock of forBlocks) {
1613
+ const vn = forBlock.varName;
1614
+ const { itemVar, indexVar, source, keyExpr } = forBlock;
1615
+
1616
+ const signalNamesSet = new Set(signalNames);
1617
+ const computedNamesSet = new Set(computedNames);
1618
+
1619
+ // Transform the source expression
1620
+ const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1621
+
1622
+ lines.push(' this.__disposers.push(__effect(() => {');
1623
+ lines.push(` const __source = ${sourceExpr};`);
1624
+ lines.push('');
1625
+ lines.push(" const __iter = typeof __source === 'number'");
1626
+ lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
1627
+ lines.push(' : (__source || []);');
1628
+ lines.push('');
1629
+
1630
+ if (keyExpr) {
1631
+ // ── Keyed reconciliation ──
1632
+ lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
1633
+ lines.push(' const __newMap = new Map();');
1634
+ lines.push(' const __newNodes = [];');
1635
+ lines.push('');
1636
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1637
+ lines.push(` const __key = ${keyExpr};`);
1638
+ lines.push(' if (__oldMap.has(__key)) {');
1639
+ lines.push(' const node = __oldMap.get(__key);');
1640
+ lines.push(' __newMap.set(__key, node);');
1641
+ lines.push(' __newNodes.push(node);');
1642
+ lines.push(' __oldMap.delete(__key);');
1643
+ lines.push(' } else {');
1644
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1645
+ lines.push(' const node = clone.firstChild;');
1646
+
1647
+ // Setup bindings/events/show/attr/model/slots for NEW nodes only
1648
+ // (reused nodes keep their existing bindings)
1649
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1650
+
1651
+ lines.push(' __newMap.set(__key, node);');
1652
+ lines.push(' __newNodes.push(node);');
1653
+ lines.push(' }');
1654
+ lines.push(' });');
1655
+ lines.push('');
1656
+ lines.push(' // Remove nodes no longer in the list');
1657
+ lines.push(' for (const n of __oldMap.values()) n.remove();');
1658
+ lines.push('');
1659
+ lines.push(' // Reorder: insert all nodes in correct order before anchor');
1660
+ lines.push(` for (const n of __newNodes) { this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor); customElements.upgrade(n); }`);
1661
+ lines.push('');
1662
+ lines.push(` this.${vn}_nodes = __newNodes;`);
1663
+ lines.push(` this.${vn}_keyMap = __newMap;`);
1664
+ lines.push(' }));');
1665
+ } else {
1666
+ // ── Non-keyed: destroy all and recreate (original behavior) ──
1667
+ lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
1668
+ lines.push(` this.${vn}_nodes = [];`);
1669
+ lines.push('');
1670
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1671
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1672
+ lines.push(' const node = clone.firstChild;');
1673
+
1674
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1675
+
1676
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1677
+ lines.push(' customElements.upgrade(node);');
1678
+ lines.push(` this.${vn}_nodes.push(node);`);
1679
+ lines.push(' });');
1680
+ lines.push(' }));');
1681
+ }
1682
+ }
1683
+
1684
+ // ── dynamic component effects ──
1685
+ for (const dyn of dynamicComponents) {
1686
+ const vn = dyn.varName;
1687
+ const isExpr = transformExpr(dyn.isExpression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1688
+ lines.push(' this.__disposers.push(__effect(() => {');
1689
+ lines.push(` const __tag = ${isExpr};`);
1690
+ lines.push(` if (__tag === this.${vn}_tag) return;`);
1691
+ lines.push(` if (this.${vn}_current) {`);
1692
+ lines.push(` this.${vn}_propDisposers.forEach(d => d());`);
1693
+ lines.push(` this.${vn}_propDisposers = [];`);
1694
+ lines.push(` this.${vn}_current.remove();`);
1695
+ lines.push(` this.${vn}_current = null;`);
1696
+ lines.push(' }');
1697
+ lines.push(' if (__tag) {');
1698
+ lines.push(' const el = document.createElement(__tag);');
1699
+ // Emit nested prop effects
1700
+ for (const prop of dyn.props) {
1701
+ const propExprTransformed = transformExpr(prop.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1702
+ lines.push(` this.${vn}_propDisposers.push(__effect(() => {`);
1703
+ lines.push(` el.setAttribute('${prop.attr}', ${propExprTransformed});`);
1704
+ lines.push(' }));');
1705
+ }
1706
+ // Emit event listeners
1707
+ for (const evt of dyn.events) {
1708
+ const handlerExpr = generateEventHandler(evt.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
1709
+ lines.push(` el.addEventListener('${evt.event}', ${handlerExpr});`);
1710
+ }
1711
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(el, this.${vn}_anchor);`);
1712
+ lines.push(' customElements.upgrade(el);');
1713
+ lines.push(` this.${vn}_current = el;`);
1714
+ lines.push(' }');
1715
+ lines.push(` this.${vn}_tag = __tag;`);
1716
+ lines.push(' }));');
1717
+ }
1718
+
1719
+ // Lifecycle: onMount hooks (at the very end of connectedCallback)
1720
+ for (const hook of onMountHooks) {
1721
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1722
+ if (hook.async) {
1723
+ lines.push(' ;(async () => {');
1724
+ const bodyLines = body.split('\n');
1725
+ for (const line of bodyLines) {
1726
+ lines.push(` ${line}`);
1727
+ }
1728
+ lines.push(' })();');
1729
+ } else {
1730
+ const bodyLines = body.split('\n');
1731
+ for (const line of bodyLines) {
1732
+ const trimmed = line.trimEnd();
1733
+ const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1734
+ lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1735
+ }
1736
+ }
1737
+ }
1738
+
1739
+ // Close connectedCallback
1740
+ lines.push(' }');
1741
+ lines.push('');
1742
+
1743
+ // disconnectedCallback (cleanup: abort listeners + dispose effects + user hooks)
1744
+ lines.push(' disconnectedCallback() {');
1745
+ lines.push(' this.__connected = false;');
1746
+ lines.push(' this.__ac.abort();');
1747
+ lines.push(' this.__disposers.forEach(d => d());');
1748
+ if (onDestroyHooks.length > 0) {
1749
+ for (const hook of onDestroyHooks) {
1750
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1751
+ if (hook.async) {
1752
+ lines.push(' ;(async () => {');
1753
+ const bodyLines = body.split('\n');
1754
+ for (const line of bodyLines) {
1755
+ lines.push(` ${line}`);
1756
+ }
1757
+ lines.push(' })();');
1758
+ } else {
1759
+ const bodyLines = body.split('\n');
1760
+ for (const line of bodyLines) {
1761
+ const trimmed = line.trimEnd();
1762
+ const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1763
+ lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1764
+ }
1765
+ }
1766
+ }
1767
+ }
1768
+ lines.push(' }');
1769
+ lines.push('');
1770
+
1771
+ // adoptedCallback (if onAdopt hooks exist)
1772
+ if (onAdoptHooks.length > 0) {
1773
+ lines.push(' adoptedCallback() {');
1774
+ for (const hook of onAdoptHooks) {
1775
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1776
+ if (hook.async) {
1777
+ lines.push(' ;(async () => {');
1778
+ const bodyLines = body.split('\n');
1779
+ for (const line of bodyLines) {
1780
+ lines.push(` ${line}`);
1781
+ }
1782
+ lines.push(' })();');
1783
+ } else {
1784
+ const bodyLines = body.split('\n');
1785
+ for (const line of bodyLines) {
1786
+ const trimmed = line.trimEnd();
1787
+ const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
1788
+ lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
1789
+ }
1790
+ }
1791
+ }
1792
+ lines.push(' }');
1793
+ lines.push('');
1794
+ }
1795
+
1796
+ // attributeChangedCallback (if props or model props exist)
1797
+ if (propDefs.length > 0 || modelDefs.length > 0) {
1798
+ lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
1799
+ for (const p of propDefs) {
1800
+ const defaultVal = p.default;
1801
+ let updateExpr;
1802
+
1803
+ if (defaultVal === 'true' || defaultVal === 'false') {
1804
+ // Boolean coercion: attribute presence = true
1805
+ updateExpr = `this._s_${p.name}(newVal != null)`;
1806
+ } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
1807
+ // Number coercion
1808
+ updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
1809
+ } else if (defaultVal === 'undefined') {
1810
+ // Undefined default pass through
1811
+ updateExpr = `this._s_${p.name}(newVal)`;
1812
+ } else {
1813
+ // String default — use nullish coalescing
1814
+ updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
1815
+ }
1816
+
1817
+ lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
1818
+ }
1819
+
1820
+ // Model props update signal directly (NO event emission)
1821
+ for (let i = 0; i < modelDefs.length; i++) {
1822
+ const md = modelDefs[i];
1823
+ const attrName = modelAttrNames[i];
1824
+ const camelName = md.name;
1825
+ const defaultVal = md.default;
1826
+ let updateExpr;
1827
+
1828
+ if (defaultVal === 'true' || defaultVal === 'false') {
1829
+ // Boolean coercion: attribute presence = true
1830
+ updateExpr = `this._m_${md.name}(newVal != null)`;
1831
+ } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
1832
+ // Number coercion
1833
+ updateExpr = `this._m_${md.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
1834
+ } else if (defaultVal === 'undefined') {
1835
+ // Undefined default — pass through
1836
+ updateExpr = `this._m_${md.name}(newVal)`;
1837
+ } else {
1838
+ // String defaultuse nullish coalescing
1839
+ updateExpr = `this._m_${md.name}(newVal ?? ${defaultVal})`;
1840
+ }
1841
+
1842
+ // Handle both kebab-case (native HTML) and camelCase (Vue) attribute names
1843
+ if (attrName !== camelName) {
1844
+ lines.push(` if (name === '${attrName}' || name === '${camelName}') ${updateExpr};`);
1845
+ } else {
1846
+ lines.push(` if (name === '${attrName}') ${updateExpr};`);
1847
+ }
1848
+ }
1849
+
1850
+ lines.push(' }');
1851
+ lines.push('');
1852
+
1853
+ // Public getters and setters
1854
+ for (const p of propDefs) {
1855
+ lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
1856
+ lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
1857
+ lines.push('');
1858
+ }
1859
+
1860
+ // Public getters and setters for model props
1861
+ for (let i = 0; i < modelDefs.length; i++) {
1862
+ const md = modelDefs[i];
1863
+ const attrName = modelAttrNames[i];
1864
+ lines.push(` get ${md.name}() { return this._m_${md.name}(); }`);
1865
+ lines.push(` set ${md.name}(val) { this._m_${md.name}(val); this.setAttribute('${attrName}', String(val)); }`);
1866
+ lines.push('');
1867
+ }
1868
+ }
1869
+
1870
+ // _emit method (if emits declared)
1871
+ // Emits the original event name + lowercase-no-hyphens for React 19 compatibility.
1872
+ // React 19 maps `oncountchanged` → addEventListener('countchanged').
1873
+ if (emits.length > 0) {
1874
+ lines.push(' _emit(name, detail) {');
1875
+ lines.push(' const evt = { detail, bubbles: true, composed: true };');
1876
+ lines.push(' this.dispatchEvent(new CustomEvent(name, evt));');
1877
+ lines.push(" const lower = name.replace(/-/g, '').toLowerCase();");
1878
+ lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
1879
+ lines.push(' }');
1880
+ lines.push('');
1881
+ }
1882
+
1883
+ // _modelSet methods (one per defineModel prop — emits events on internal write)
1884
+ // Emits:
1885
+ // 1. wcc:model — canonical event for vanilla JS, WCC-to-WCC, React adapter, Vue plugin
1886
+ // 2. propNameChange — for Angular [(prop)] banana-box syntax (zero-config)
1887
+ for (const md of modelDefs) {
1888
+ lines.push(` _modelSet_${md.name}(newVal) {`);
1889
+ lines.push(` const oldVal = this._m_${md.name}();`);
1890
+ lines.push(` this._m_${md.name}(newVal);`);
1891
+ lines.push(` this.dispatchEvent(new CustomEvent('wcc:model', {`);
1892
+ lines.push(` detail: { prop: '${md.name}', value: newVal, oldValue: oldVal },`);
1893
+ lines.push(` bubbles: true,`);
1894
+ lines.push(` composed: true`);
1895
+ lines.push(` }));`);
1896
+ lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
1897
+ lines.push(' }');
1898
+ lines.push('');
1899
+ }
1900
+
1901
+ // __scopedSlots instance getter and registerSlotRenderer (if scoped slots exist)
1902
+ if (scopedSlotNames.length > 0) {
1903
+ lines.push(' get __scopedSlots() { return this.constructor.__scopedSlots || []; }');
1904
+ lines.push('');
1905
+ lines.push(' registerSlotRenderer(slotName, callback) {');
1906
+ lines.push(' if (!this.__slotRenderers) this.__slotRenderers = {};');
1907
+ lines.push(' this.__slotRenderers[slotName] = callback;');
1908
+ lines.push(' if (this.__slotProps && this.__slotProps[slotName]) {');
1909
+ lines.push(' callback(this.__slotProps[slotName]);');
1910
+ lines.push(' }');
1911
+ lines.push(' return () => {');
1912
+ lines.push(' if (this.__slotRenderers) {');
1913
+ lines.push(' delete this.__slotRenderers[slotName];');
1914
+ lines.push(' }');
1915
+ lines.push(' };');
1916
+ lines.push(' }');
1917
+ lines.push('');
1918
+ }
1919
+
1920
+ // User methods (prefixed with _)
1921
+ if (methods.length > 0 && options.comments) lines.push('');
1922
+ if (methods.length > 0 && options.comments) lines.push(' // --- Methods ---');
1923
+ for (const m of methods) {
1924
+ const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
1925
+ lines.push(` _${m.name}(${m.params}) {`);
1926
+ const bodyLines = body.split('\n');
1927
+ for (const line of bodyLines) {
1928
+ lines.push(` ${line}`);
1929
+ }
1930
+ lines.push(' }');
1931
+ lines.push('');
1932
+ }
1933
+
1934
+ // ── Ref getter properties ──
1935
+ for (const rd of refs) {
1936
+ // Find matching RefBinding
1937
+ const rb = refBindings.find(b => b.refName === rd.refName);
1938
+ if (rb) {
1939
+ lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
1940
+ lines.push('');
1941
+ }
1942
+ }
1943
+
1944
+ // ── defineExpose: public getters/methods ──
1945
+ for (const name of exposeNames) {
1946
+ if (computedNames.includes(name)) {
1947
+ lines.push(` get ${name}() { return this._c_${name}(); }`);
1948
+ } else if (signalNames.includes(name)) {
1949
+ lines.push(` get ${name}() { return this._${name}(); }`);
1950
+ } else if (methodNames.includes(name)) {
1951
+ lines.push(` ${name}(...args) { return this._${name}(...args); }`);
1952
+ } else if (constantNames.includes(name)) {
1953
+ lines.push(` get ${name}() { return this._const_${name}; }`);
1954
+ }
1955
+ }
1956
+ if (exposeNames.length > 0) lines.push('');
1957
+
1958
+ // ── if setup methods ──
1959
+ for (const ifBlock of ifBlocks) {
1960
+ const vn = ifBlock.varName;
1961
+ const hasSetup = ifBlock.branches.some(b =>
1962
+ (b.bindings && b.bindings.length > 0) ||
1963
+ (b.events && b.events.length > 0) ||
1964
+ (b.showBindings && b.showBindings.length > 0) ||
1965
+ (b.attrBindings && b.attrBindings.length > 0) ||
1966
+ (b.modelBindings && b.modelBindings.length > 0)
1967
+ );
1968
+ if (!hasSetup) continue;
1969
+
1970
+ lines.push(` ${vn}_setup(node, branch) {`);
1971
+ for (let i = 0; i < ifBlock.branches.length; i++) {
1972
+ const branch = ifBlock.branches[i];
1973
+ const hasBranchSetup =
1974
+ (branch.bindings && branch.bindings.length > 0) ||
1975
+ (branch.events && branch.events.length > 0) ||
1976
+ (branch.showBindings && branch.showBindings.length > 0) ||
1977
+ (branch.attrBindings && branch.attrBindings.length > 0) ||
1978
+ (branch.modelBindings && branch.modelBindings.length > 0);
1979
+ if (!hasBranchSetup) continue;
1980
+
1981
+ const keyword = i === 0 ? 'if' : 'else if';
1982
+ lines.push(` ${keyword} (branch === ${i}) {`);
1983
+
1984
+ // Bindings: generate DOM refs and effects for text bindings
1985
+ for (const b of branch.bindings) {
1986
+ lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
1987
+ if (b.type === 'prop') {
1988
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
1989
+ } else if (b.type === 'signal') {
1990
+ const modelPropName = modelVarMap.get(b.name);
1991
+ const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
1992
+ lines.push(` __effect(() => { ${b.varName}.textContent = ${signalRef} ?? ''; });`);
1993
+ } else if (b.type === 'computed') {
1994
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
1995
+ } else {
1996
+ // method/expression type check for props.x pattern
1997
+ let ref;
1998
+ if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
1999
+ const propName = b.name.slice(propsObjectName.length + 1);
2000
+ ref = `this._s_${propName}()`;
2001
+ } else {
2002
+ ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
2003
+ }
2004
+ lines.push(` __effect(() => { ${b.varName}.textContent = ${ref} ?? ''; });`);
2005
+ }
2006
+ }
2007
+
2008
+ // Events: generate addEventListener
2009
+ for (const e of branch.events) {
2010
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
2011
+ lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
2012
+ lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
2013
+ }
2014
+
2015
+ // Show bindings: generate effects
2016
+ for (const sb of branch.showBindings) {
2017
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
2018
+ lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
2019
+ lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
2020
+ }
2021
+
2022
+ // Attr bindings: generate effects
2023
+ for (const ab of branch.attrBindings) {
2024
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
2025
+ lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
2026
+ lines.push(` __effect(() => {`);
2027
+ lines.push(` const __val = ${expr};`);
2028
+ lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
2029
+ lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
2030
+ lines.push(` });`);
2031
+ }
2032
+
2033
+ // Model bindings: generate effects and listeners
2034
+ for (const mb of (branch.modelBindings || [])) {
2035
+ const nodeRef = pathExpr(mb.path, 'node');
2036
+ lines.push(` const ${mb.varName} = ${nodeRef};`);
2037
+ // Effect (signal → DOM)
2038
+ lines.push(` __effect(() => {`);
2039
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
2040
+ lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
2041
+ } else if (mb.prop === 'checked') {
2042
+ lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
2043
+ } else {
2044
+ lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
2045
+ }
2046
+ lines.push(` });`);
2047
+ // Listener (DOM → signal)
2048
+ if (mb.prop === 'checked' && mb.radioValue === null) {
2049
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
2050
+ } else if (mb.coerce) {
2051
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
2052
+ } else {
2053
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
2054
+ }
2055
+ }
2056
+
2057
+ lines.push(' }');
2058
+ }
2059
+ lines.push(' }');
2060
+ lines.push('');
2061
+ }
2062
+
2063
+ lines.push('}');
2064
+ lines.push('');
2065
+
2066
+ // ── 5. Custom element registration ──
2067
+ lines.push(`if (!customElements.get('${tagName}')) customElements.define('${tagName}', ${className});`);
2068
+ lines.push('');
2069
+
2070
+ // ── 6. Default export (enables named imports from parent components) ──
2071
+ lines.push(`export default ${className};`);
2072
+
2073
+ return lines.join('\n');
2074
+ }