@sprlab/wccompiler 0.0.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/codegen.js CHANGED
@@ -1,29 +1,298 @@
1
1
  /**
2
- * Code Generator for compiled web components.
2
+ * Code Generator for wcCompiler v2.
3
3
  *
4
- * Takes a complete ParseResult (with bindings, events, slots populated by tree-walker)
4
+ * Takes a complete ParseResult (with bindings, events populated by tree-walker)
5
5
  * and produces a self-contained JavaScript string with:
6
6
  * - Inline mini reactive runtime (zero imports)
7
7
  * - Scoped CSS injection
8
- * - HTMLElement class with signals, effects, slots, events
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.
9
14
  */
10
15
 
11
16
  import { reactiveRuntime } from './reactive-runtime.js';
12
17
  import { scopeCSS } from './css-scoper.js';
13
18
 
19
+ /** @import { ParseResult } from './types.js' */
20
+
14
21
  /**
15
22
  * Convert a path array to a JS expression string.
16
- * Inlined here to avoid pulling in jsdom via tree-walker.js.
17
23
  * e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
24
+ *
25
+ * @param {string[]} parts
26
+ * @param {string} rootVar
27
+ * @returns {string}
18
28
  */
19
- function pathExpr(parts, rootVar) {
29
+ export function pathExpr(parts, rootVar) {
20
30
  return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
21
31
  }
22
32
 
33
+ /**
34
+ * Escape special regex characters in a string.
35
+ *
36
+ * @param {string} str
37
+ * @returns {string}
38
+ */
39
+ function escapeRegex(str) {
40
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
41
+ }
42
+
43
+ /**
44
+ * Get the signal reference for a slot prop source expression.
45
+ *
46
+ * @param {string} source — Source variable name from :prop="source"
47
+ * @param {string[]} signalNames — Signal variable names
48
+ * @param {string[]} computedNames — Computed variable names
49
+ * @param {Set<string>} propNames — Prop names from defineProps
50
+ * @returns {string}
51
+ */
52
+ function slotPropRef(source, signalNames, computedNames, propNames) {
53
+ if (propNames.has(source)) return `this._s_${source}()`;
54
+ if (computedNames.includes(source)) return `this._c_${source}()`;
55
+ if (signalNames.includes(source)) return `this._${source}()`;
56
+ return `'${source}'`;
57
+ }
58
+
59
+ /**
60
+ * Transform an expression by rewriting signal/computed variable references
61
+ * to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
62
+ *
63
+ * Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
64
+ * Also handles `emitsObjectName(` → `this._emit(` transformation.
65
+ *
66
+ * Uses word-boundary regex for each known signal/computed name.
67
+ * Does NOT transform if the name is followed by `.set(` (that's a write,
68
+ * handled by transformMethodBody).
69
+ *
70
+ * @param {string} expr — Expression to transform
71
+ * @param {string[]} signalNames — Signal variable names
72
+ * @param {string[]} computedNames — Computed variable names
73
+ * @param {string|null} [propsObjectName] — Props object variable name
74
+ * @param {Set<string>} [propNames] — Set of prop names
75
+ * @param {string|null} [emitsObjectName] — Emits object variable name
76
+ * @returns {string}
77
+ */
78
+ export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = []) {
79
+ let result = expr;
80
+
81
+ // Transform emit calls: emitsObjectName( → this._emit(
82
+ if (emitsObjectName) {
83
+ const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
84
+ result = result.replace(emitsRe, 'this._emit(');
85
+ }
86
+
87
+ // Transform props.x → this._s_x() BEFORE signal/computed transforms
88
+ if (propsObjectName && propNames.size > 0) {
89
+ const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
90
+ result = result.replace(propsRe, (match, propName) => {
91
+ if (propNames.has(propName)) {
92
+ return `this._s_${propName}()`;
93
+ }
94
+ return match; // leave unknown props unchanged
95
+ });
96
+ }
97
+
98
+ // Transform computed names first (to avoid partial matches with signals)
99
+ for (const name of computedNames) {
100
+ // Skip propsObjectName and emitsObjectName
101
+ if (propsObjectName && name === propsObjectName) continue;
102
+ if (emitsObjectName && name === emitsObjectName) continue;
103
+ // First: transform name() calls → this._c_name() (replace the call, not just the name)
104
+ const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
105
+ result = result.replace(callRe, `this._c_${name}()`);
106
+ // Then: transform bare name references (not followed by ( or .set() → this._c_name()
107
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
108
+ result = result.replace(bareRe, `this._c_${name}()`);
109
+ }
110
+
111
+ // Transform signal names
112
+ for (const name of signalNames) {
113
+ // Skip propsObjectName and emitsObjectName
114
+ if (propsObjectName && name === propsObjectName) continue;
115
+ if (emitsObjectName && name === emitsObjectName) continue;
116
+ // First: transform name() calls → this._name() (replace the call, not just the name)
117
+ const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
118
+ result = result.replace(callRe, `this._${name}()`);
119
+ // Then: transform bare name references (not followed by ( or .set() → this._name()
120
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
121
+ result = result.replace(bareRe, `this._${name}()`);
122
+ }
123
+
124
+ // Transform constant names → this._const_name (no function call)
125
+ for (const name of constantNames) {
126
+ if (propsObjectName && name === propsObjectName) continue;
127
+ if (emitsObjectName && name === emitsObjectName) continue;
128
+ const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
129
+ result = result.replace(bareRe, `this._const_${name}`);
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Transform a method/effect body by rewriting signal writes and reads.
137
+ *
138
+ * - `emitsObjectName(` → `this._emit(` (emit call)
139
+ * - `props.x` → `this._s_x()` (prop access)
140
+ * - `x.set(value)` → `this._x(value)` (signal write via setter)
141
+ * - `x()` → `this._x()` (signal read)
142
+ * - Computed `x()` → `this._c_x()` (computed read)
143
+ *
144
+ * @param {string} body — Function body to transform
145
+ * @param {string[]} signalNames — Signal variable names
146
+ * @param {string[]} computedNames — Computed variable names
147
+ * @param {string|null} [propsObjectName] — Props object variable name
148
+ * @param {Set<string>} [propNames] — Set of prop names
149
+ * @param {string|null} [emitsObjectName] — Emits object variable name
150
+ * @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
151
+ * @returns {string}
152
+ */
153
+ export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = []) {
154
+ let result = body;
155
+
156
+ // 0a. Transform emit calls: emitsObjectName( → this._emit(
157
+ if (emitsObjectName) {
158
+ const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
159
+ result = result.replace(emitsRe, 'this._emit(');
160
+ }
161
+
162
+ // 0b. Transform props.x → this._s_x() BEFORE other transforms
163
+ if (propsObjectName && propNames.size > 0) {
164
+ const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
165
+ result = result.replace(propsRe, (match, propName) => {
166
+ if (propNames.has(propName)) {
167
+ return `this._s_${propName}()`;
168
+ }
169
+ return match;
170
+ });
171
+ }
172
+
173
+ // 0c. Transform ref access: varName.value → this._varName.value
174
+ for (const name of refVarNames) {
175
+ const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
176
+ result = result.replace(refRe, `this._${name}.value`);
177
+ }
178
+
179
+ // 1. Transform signal writes: x.set(value) → this._x(value)
180
+ for (const name of signalNames) {
181
+ if (propsObjectName && name === propsObjectName) continue;
182
+ if (emitsObjectName && name === emitsObjectName) continue;
183
+ const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
184
+ result = result.replace(setRe, `this._${name}(`);
185
+ }
186
+
187
+ // 2. Transform computed reads: x() → this._c_x()
188
+ for (const name of computedNames) {
189
+ if (propsObjectName && name === propsObjectName) continue;
190
+ if (emitsObjectName && name === emitsObjectName) continue;
191
+ const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
192
+ result = result.replace(readRe, `this._c_${name}()`);
193
+ }
194
+
195
+ // 3. Transform signal reads: x() → this._x()
196
+ for (const name of signalNames) {
197
+ if (propsObjectName && name === propsObjectName) continue;
198
+ if (emitsObjectName && name === emitsObjectName) continue;
199
+ const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
200
+ result = result.replace(readRe, `this._${name}()`);
201
+ }
202
+
203
+ // 4. Transform constant reads: name → this._const_name
204
+ for (const name of constantNames) {
205
+ if (propsObjectName && name === propsObjectName) continue;
206
+ if (emitsObjectName && name === emitsObjectName) continue;
207
+ const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
208
+ result = result.replace(bareRe, `this._const_${name}`);
209
+ }
210
+
211
+ return result;
212
+ }
213
+
214
+ /**
215
+ * Transform an expression within the scope of an each block.
216
+ * - References to itemVar and indexVar are left UNTRANSFORMED
217
+ * - References to component variables (props, reactive, computed) ARE transformed
218
+ *
219
+ * @param {string} expr - The expression to transform
220
+ * @param {string} itemVar - Name of the iteration variable
221
+ * @param {string | null} indexVar - Name of the index variable
222
+ * @param {Set<string>} propsSet
223
+ * @param {Set<string>} rootVarNames - Set of signal names
224
+ * @param {Set<string>} computedNames
225
+ * @returns {string}
226
+ */
227
+ export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
228
+ let r = expr;
229
+ const excludeSet = new Set([itemVar]);
230
+ if (indexVar) excludeSet.add(indexVar);
231
+
232
+ for (const p of propsSet) {
233
+ if (excludeSet.has(p)) continue;
234
+ r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
235
+ }
236
+ for (const n of rootVarNames) {
237
+ if (excludeSet.has(n)) continue;
238
+ r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
239
+ }
240
+ for (const n of computedNames) {
241
+ if (excludeSet.has(n)) continue;
242
+ r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
243
+ }
244
+ return r;
245
+ }
246
+
247
+ /**
248
+ * Check if a binding name is static within an each scope (references only item/index).
249
+ * A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
250
+ *
251
+ * @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
252
+ * @param {string} itemVar
253
+ * @param {string | null} indexVar
254
+ * @returns {boolean}
255
+ */
256
+ export function isStaticForBinding(name, itemVar, indexVar) {
257
+ if (name === itemVar || name.startsWith(itemVar + '.')) return true;
258
+ if (indexVar && name === indexVar) return true;
259
+ return false;
260
+ }
261
+
262
+ /**
263
+ * Check if an expression is static within an each scope (references only item/index, no component vars).
264
+ *
265
+ * @param {string} expr
266
+ * @param {string} itemVar
267
+ * @param {string | null} indexVar
268
+ * @param {Set<string>} propsSet
269
+ * @param {Set<string>} rootVarNames
270
+ * @param {Set<string>} computedNames
271
+ * @returns {boolean}
272
+ */
273
+ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
274
+ const excludeSet = new Set([itemVar]);
275
+ if (indexVar) excludeSet.add(indexVar);
276
+
277
+ for (const p of propsSet) {
278
+ if (excludeSet.has(p)) continue;
279
+ if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
280
+ }
281
+ for (const n of rootVarNames) {
282
+ if (excludeSet.has(n)) continue;
283
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
284
+ }
285
+ for (const n of computedNames) {
286
+ if (excludeSet.has(n)) continue;
287
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
288
+ }
289
+ return true;
290
+ }
291
+
23
292
  /**
24
293
  * Generate a fully self-contained JS component from a ParseResult.
25
294
  *
26
- * @param {import('./parser.js').ParseResult} parseResult - Complete IR with bindings/events/slots
295
+ * @param {ParseResult} parseResult Complete IR with bindings/events
27
296
  * @returns {string} JavaScript source code
28
297
  */
29
298
  export function generateComponent(parseResult) {
@@ -31,28 +300,53 @@ export function generateComponent(parseResult) {
31
300
  tagName,
32
301
  className,
33
302
  style,
34
- props,
35
- reactiveVars,
303
+ signals,
36
304
  computeds,
37
- watchers,
305
+ effects,
38
306
  methods,
39
307
  bindings,
40
308
  events,
41
- slots,
42
309
  processedTemplate,
310
+ propDefs = [],
311
+ propsObjectName = null,
312
+ emits = [],
313
+ emitsObjectName = null,
314
+ ifBlocks = [],
315
+ showBindings = [],
316
+ forBlocks = [],
317
+ onMountHooks = [],
318
+ onDestroyHooks = [],
319
+ modelBindings = [],
320
+ attrBindings = [],
321
+ slots = [],
322
+ constantVars = [],
323
+ refs = [],
324
+ refBindings = [],
325
+ childComponents = [],
326
+ childImports = [],
43
327
  } = parseResult;
44
328
 
45
- const propsSet = new Set(props);
46
- const computedNames = new Set(computeds.map(c => c.name));
47
- const rootVarNames = new Set(reactiveVars.map(v => v.name));
329
+ const signalNames = signals.map(s => s.name);
330
+ const computedNames = computeds.map(c => c.name);
331
+ const constantNames = constantVars.map(v => v.name);
332
+ const refVarNames = refs.map(r => r.varName);
333
+ const propNames = new Set(propDefs.map(p => p.name));
48
334
 
49
335
  const lines = [];
50
336
 
51
- // ── 1. Inline reactive runtime (task 6.1) ──
337
+ // ── 1. Inline reactive runtime ──
52
338
  lines.push(reactiveRuntime.trim());
53
339
  lines.push('');
54
340
 
55
- // ── 2. CSS injection (task 6.6) ──
341
+ // ── 1b. Child component imports ──
342
+ for (const ci of childImports) {
343
+ lines.push(`import '${ci.importPath}';`);
344
+ }
345
+ if (childImports.length > 0) {
346
+ lines.push('');
347
+ }
348
+
349
+ // ── 2. CSS injection (scoped CSS into document.head, always) ──
56
350
  if (style) {
57
351
  const scoped = scopeCSS(style, tagName);
58
352
  lines.push(`const __css_${className} = document.createElement('style');`);
@@ -61,25 +355,26 @@ export function generateComponent(parseResult) {
61
355
  lines.push('');
62
356
  }
63
357
 
64
- // ── 3. Template ──
358
+ // ── 3. Template element ──
65
359
  lines.push(`const __t_${className} = document.createElement('template');`);
66
- lines.push(`__t_${className}.innerHTML = \`${processedTemplate}\`;`);
360
+ lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
67
361
  lines.push('');
68
362
 
69
- // ── 4. Class definition (task 6.1) ──
363
+ // ── 4. HTMLElement class ──
70
364
  lines.push(`class ${className} extends HTMLElement {`);
71
365
 
72
- // observedAttributes
73
- lines.push(' static get observedAttributes() {');
74
- lines.push(` return [${props.map(p => `'${p}'`).join(', ')}];`);
75
- lines.push(' }');
76
- lines.push('');
366
+ // Static observedAttributes (if props exist)
367
+ if (propDefs.length > 0) {
368
+ const attrNames = propDefs.map(p => `'${p.attrName}'`).join(', ');
369
+ lines.push(` static get observedAttributes() { return [${attrNames}]; }`);
370
+ lines.push('');
371
+ }
77
372
 
78
- // constructor
373
+ // Constructor
79
374
  lines.push(' constructor() {');
80
375
  lines.push(' super();');
81
376
 
82
- // Slot resolution code (task 6.5) must read childNodes BEFORE replacing innerHTML
377
+ // Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
83
378
  if (slots.length > 0) {
84
379
  lines.push(' const __slotMap = {};');
85
380
  lines.push(' const __defaultSlotNodes = [];');
@@ -97,48 +392,121 @@ export function generateComponent(parseResult) {
97
392
  lines.push(' }');
98
393
  }
99
394
 
100
- // Clone template and assign DOM refs
395
+ // Clone template
101
396
  lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
102
397
 
103
- const allNodes = [...bindings, ...events, ...slots];
104
- for (const n of allNodes) {
105
- lines.push(` this.${n.varName} = ${pathExpr(n.path, '__root')};`);
398
+ // Assign DOM refs for bindings
399
+ for (const b of bindings) {
400
+ lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
106
401
  }
107
402
 
108
- lines.push(" this.innerHTML = '';");
109
- lines.push(' this.appendChild(__root);');
403
+ // Assign DOM refs for events
404
+ for (const e of events) {
405
+ lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
406
+ }
407
+
408
+ // Assign DOM refs for show bindings
409
+ for (const sb of showBindings) {
410
+ lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
411
+ }
412
+
413
+ // Assign DOM refs for model bindings
414
+ for (const mb of modelBindings) {
415
+ lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
416
+ }
110
417
 
111
- // Static slot injection (task 6.5)
418
+ // Assign DOM refs for slot placeholders
112
419
  for (const s of slots) {
113
- if (s.name && s.slotProps.length > 0) {
114
- // Scoped slot: store template for reactive effect
115
- lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
116
- } else if (s.name) {
117
- // Named slot: inject content directly
118
- lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
420
+ lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
421
+ }
422
+
423
+ // Assign DOM refs for child component instances
424
+ for (const cc of childComponents) {
425
+ lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
426
+ }
427
+
428
+ // Assign DOM refs for attr bindings (reuse ref when same path)
429
+ const attrPathMap = new Map();
430
+ for (const ab of attrBindings) {
431
+ const pathKey = ab.path.join('.');
432
+ if (attrPathMap.has(pathKey)) {
433
+ lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
119
434
  } else {
120
- // Default slot
121
- lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
435
+ lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
436
+ attrPathMap.set(pathKey, ab.varName);
122
437
  }
123
438
  }
124
439
 
125
- // Signal inits (task 6.2)
126
- for (const p of props) {
127
- lines.push(` this._s_${p} = __signal(null);`);
440
+ // Prop signal initialization (BEFORE user signals)
441
+ for (const p of propDefs) {
442
+ lines.push(` this._s_${p.name} = __signal(${p.default});`);
443
+ }
444
+
445
+ // Signal initialization
446
+ for (const s of signals) {
447
+ lines.push(` this._${s.name} = __signal(${s.value});`);
128
448
  }
129
- for (const v of reactiveVars) {
130
- lines.push(` this._${v.name} = __signal(${v.value});`);
449
+
450
+ // Constant initialization
451
+ for (const c of constantVars) {
452
+ lines.push(` this._const_${c.name} = ${c.value};`);
131
453
  }
132
454
 
133
- // Computed inits (task 6.2)
455
+ // Computed initialization
134
456
  for (const c of computeds) {
135
- const body = transformExpr(c.body, propsSet, rootVarNames, computedNames);
457
+ const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
136
458
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
137
459
  }
138
460
 
139
- // Watcher prev inits (task 6.3)
140
- for (const w of watchers) {
141
- lines.push(` this.__prev_${w.target} = undefined;`);
461
+ // ── if: template creation, anchor reference, state init ──
462
+ for (const ifBlock of ifBlocks) {
463
+ const vn = ifBlock.varName;
464
+ // Template per branch
465
+ for (let i = 0; i < ifBlock.branches.length; i++) {
466
+ const branch = ifBlock.branches[i];
467
+ lines.push(` this.${vn}_t${i} = document.createElement('template');`);
468
+ lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
469
+ }
470
+ // Reference to anchor comment node (must be before appendChild moves nodes out of __root)
471
+ lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
472
+ // Active branch state
473
+ lines.push(` this.${vn}_current = null;`);
474
+ lines.push(` this.${vn}_active = undefined;`);
475
+ }
476
+
477
+ // ── each: template creation, anchor reference, nodes array ──
478
+ for (const forBlock of forBlocks) {
479
+ const vn = forBlock.varName;
480
+ lines.push(` this.${vn}_tpl = document.createElement('template');`);
481
+ lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
482
+ lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
483
+ lines.push(` this.${vn}_nodes = [];`);
484
+ }
485
+
486
+ // ── Ref DOM reference assignments (before appendChild moves nodes) ──
487
+ for (const rb of refBindings) {
488
+ lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
489
+ }
490
+
491
+ // Append DOM (always light DOM)
492
+ lines.push(" this.innerHTML = '';");
493
+ lines.push(' this.appendChild(__root);');
494
+
495
+ // Static slot injection (after DOM is appended)
496
+ for (const s of slots) {
497
+ if (s.name && s.slotProps.length > 0) {
498
+ // Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
499
+ lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
500
+ if (s.defaultContent) {
501
+ lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
502
+ }
503
+ } else if (s.name) {
504
+ // Named slot: inject content directly
505
+ lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
506
+ } else {
507
+ // Default slot: inject collected child nodes
508
+ lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
509
+ }
142
510
  }
143
511
 
144
512
  lines.push(' }');
@@ -147,19 +515,35 @@ export function generateComponent(parseResult) {
147
515
  // connectedCallback
148
516
  lines.push(' connectedCallback() {');
149
517
 
150
- // Binding effects (task 6.2)
151
- if (bindings.length > 0) {
152
- lines.push(' __effect(() => {');
153
- for (const b of bindings) {
154
- lines.push(` this.${b.varName}.textContent = ${bindingRef(b)} ?? '';`);
518
+ // Binding effects one __effect per binding
519
+ for (const b of bindings) {
520
+ if (b.type === 'prop') {
521
+ lines.push(' __effect(() => {');
522
+ lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
523
+ lines.push(' });');
524
+ } else if (b.type === 'signal') {
525
+ lines.push(' __effect(() => {');
526
+ lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
527
+ lines.push(' });');
528
+ } else if (b.type === 'computed') {
529
+ lines.push(' __effect(() => {');
530
+ lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
531
+ lines.push(' });');
532
+ } else {
533
+ // method type — call the method
534
+ lines.push(' __effect(() => {');
535
+ lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
536
+ lines.push(' });');
155
537
  }
156
- lines.push(' });');
157
538
  }
158
539
 
159
- // Reactive slot effects (task 6.5)
540
+ // Scoped slot effects reactive resolution of {{propName}} in consumer templates
160
541
  for (const s of slots) {
161
542
  if (s.name && s.slotProps.length > 0) {
162
- const propsObj = s.slotProps.map(sp => `${sp.prop}: ${slotPropRef(sp.source, propsSet, computedNames, rootVarNames)}`).join(', ');
543
+ const propsObj = s.slotProps.map(sp => {
544
+ const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
545
+ return `${sp.prop}: ${ref}`;
546
+ }).join(', ');
163
547
  lines.push(` if (this.__slotTpl_${s.name}) {`);
164
548
  lines.push(' __effect(() => {');
165
549
  lines.push(` const __props = { ${propsObj} };`);
@@ -173,152 +557,484 @@ export function generateComponent(parseResult) {
173
557
  }
174
558
  }
175
559
 
176
- // Watcher effects (task 6.3)
177
- for (const w of watchers) {
178
- const watchRef = signalRef(w.target, propsSet, computedNames, rootVarNames);
179
- let body = transformExpr(w.body, propsSet, rootVarNames, computedNames);
180
- body = body.replace(/\bemit\(/g, 'this._emit(');
560
+ // Child component reactive prop bindings
561
+ for (const cc of childComponents) {
562
+ for (const pb of cc.propBindings) {
563
+ let ref;
564
+ if (pb.type === 'prop') {
565
+ ref = `this._s_${pb.expr}()`;
566
+ } else if (pb.type === 'computed') {
567
+ ref = `this._c_${pb.expr}()`;
568
+ } else if (pb.type === 'signal') {
569
+ ref = `this._${pb.expr}()`;
570
+ } else if (pb.type === 'constant') {
571
+ ref = `this._const_${pb.expr}`;
572
+ } else {
573
+ ref = `this._${pb.expr}()`;
574
+ }
575
+ lines.push(' __effect(() => {');
576
+ lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
577
+ lines.push(' });');
578
+ }
579
+ }
580
+
581
+ // User effects
582
+ for (const eff of effects) {
583
+ const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
181
584
  lines.push(' __effect(() => {');
182
- lines.push(` const ${w.newParam} = ${watchRef};`);
183
- lines.push(` if (this.__prev_${w.target} !== undefined) {`);
184
- lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
185
- lines.push(` ${body}`);
186
- lines.push(' }');
187
- lines.push(` this.__prev_${w.target} = ${w.newParam};`);
585
+ // Indent each line of the body
586
+ const bodyLines = body.split('\n');
587
+ for (const line of bodyLines) {
588
+ lines.push(` ${line}`);
589
+ }
188
590
  lines.push(' });');
189
591
  }
190
592
 
191
- // Event listeners (task 6.4)
593
+ // Event listeners
192
594
  for (const e of events) {
193
595
  lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
194
596
  }
195
597
 
196
- lines.push(' }');
197
- lines.push('');
598
+ // Show effects — one __effect per ShowBinding
599
+ for (const sb of showBindings) {
600
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
601
+ lines.push(' __effect(() => {');
602
+ lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
603
+ lines.push(' });');
604
+ }
605
+
606
+ // Model effects — signal → DOM (one __effect per ModelBinding)
607
+ for (const mb of modelBindings) {
608
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
609
+ // Radio: compare signal value to radioValue
610
+ lines.push(' __effect(() => {');
611
+ lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
612
+ lines.push(' });');
613
+ } else if (mb.prop === 'checked') {
614
+ // Checkbox: coerce to boolean
615
+ lines.push(' __effect(() => {');
616
+ lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
617
+ lines.push(' });');
618
+ } else {
619
+ // Value-based (text, number, textarea, select): nullish coalesce to ''
620
+ lines.push(' __effect(() => {');
621
+ lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
622
+ lines.push(' });');
623
+ }
624
+ }
625
+
626
+ // Model event listeners — DOM → signal (one addEventListener per ModelBinding)
627
+ for (const mb of modelBindings) {
628
+ if (mb.prop === 'checked' && mb.radioValue === null) {
629
+ // Checkbox: read e.target.checked
630
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
631
+ } else if (mb.coerce) {
632
+ // Number input: wrap in Number()
633
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
634
+ } else {
635
+ // All others: read e.target.value
636
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
637
+ }
638
+ }
198
639
 
199
- // attributeChangedCallback (task 6.1)
200
- lines.push(' attributeChangedCallback(name, oldValue, newValue) {');
201
- for (const p of props) {
202
- lines.push(` if (name === '${p}') this._s_${p}(newValue);`);
640
+ // Attr binding effects — one __effect per AttrBinding
641
+ for (const ab of attrBindings) {
642
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
643
+ if (ab.kind === 'attr') {
644
+ lines.push(' __effect(() => {');
645
+ lines.push(` const __v = ${expr};`);
646
+ lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
647
+ lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
648
+ lines.push(' });');
649
+ } else if (ab.kind === 'bool') {
650
+ lines.push(' __effect(() => {');
651
+ lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
652
+ lines.push(' });');
653
+ } else if (ab.kind === 'class') {
654
+ if (ab.expression.trimStart().startsWith('{')) {
655
+ // Object expression: iterate entries, classList.add/remove
656
+ lines.push(' __effect(() => {');
657
+ lines.push(` const __obj = ${expr};`);
658
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
659
+ lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
660
+ lines.push(' }');
661
+ lines.push(' });');
662
+ } else {
663
+ // String expression: set className
664
+ lines.push(' __effect(() => {');
665
+ lines.push(` this.${ab.varName}.className = ${expr};`);
666
+ lines.push(' });');
667
+ }
668
+ } else if (ab.kind === 'style') {
669
+ if (ab.expression.trimStart().startsWith('{')) {
670
+ // Object expression: iterate entries, set style[key]
671
+ lines.push(' __effect(() => {');
672
+ lines.push(` const __obj = ${expr};`);
673
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
674
+ lines.push(` this.${ab.varName}.style[__k] = __val;`);
675
+ lines.push(' }');
676
+ lines.push(' });');
677
+ } else {
678
+ // String expression: set cssText
679
+ lines.push(' __effect(() => {');
680
+ lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
681
+ lines.push(' });');
682
+ }
683
+ }
203
684
  }
685
+
686
+ // ── if effects ──
687
+ for (const ifBlock of ifBlocks) {
688
+ const vn = ifBlock.varName;
689
+ lines.push(' __effect(() => {');
690
+ lines.push(' let __branch = null;');
691
+ for (let i = 0; i < ifBlock.branches.length; i++) {
692
+ const branch = ifBlock.branches[i];
693
+ if (branch.type === 'if') {
694
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
695
+ lines.push(` if (${expr}) { __branch = ${i}; }`);
696
+ } else if (branch.type === 'else-if') {
697
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
698
+ lines.push(` else if (${expr}) { __branch = ${i}; }`);
699
+ } else {
700
+ // else
701
+ lines.push(` else { __branch = ${i}; }`);
702
+ }
703
+ }
704
+ lines.push(` if (__branch === this.${vn}_active) return;`);
705
+ // Remove previous branch
706
+ lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
707
+ // Insert new branch
708
+ lines.push(' if (__branch !== null) {');
709
+ const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
710
+ lines.push(` const tpl = [${tplArray}][__branch];`);
711
+ lines.push(' const clone = tpl.content.cloneNode(true);');
712
+ lines.push(' const node = clone.firstChild;');
713
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
714
+ lines.push(` this.${vn}_current = node;`);
715
+ // Setup bindings/events for active branch (only if any branch has bindings/events)
716
+ const hasSetup = ifBlock.branches.some(b =>
717
+ (b.bindings && b.bindings.length > 0) ||
718
+ (b.events && b.events.length > 0) ||
719
+ (b.showBindings && b.showBindings.length > 0) ||
720
+ (b.attrBindings && b.attrBindings.length > 0) ||
721
+ (b.modelBindings && b.modelBindings.length > 0)
722
+ );
723
+ if (hasSetup) {
724
+ lines.push(` this.${vn}_setup(node, __branch);`);
725
+ }
726
+ lines.push(' }');
727
+ lines.push(` this.${vn}_active = __branch;`);
728
+ lines.push(' });');
729
+ }
730
+
731
+ // ── each effects ──
732
+ for (const forBlock of forBlocks) {
733
+ const vn = forBlock.varName;
734
+ const { itemVar, indexVar, source } = forBlock;
735
+
736
+ const signalNamesSet = new Set(signalNames);
737
+ const computedNamesSet = new Set(computedNames);
738
+
739
+ // Transform the source expression
740
+ const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
741
+
742
+ lines.push(' __effect(() => {');
743
+ lines.push(` const __source = ${sourceExpr};`);
744
+ lines.push('');
745
+ lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
746
+ lines.push(` this.${vn}_nodes = [];`);
747
+ lines.push('');
748
+ lines.push(" const __iter = typeof __source === 'number'");
749
+ lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
750
+ lines.push(' : (__source || []);');
751
+ lines.push('');
752
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
753
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
754
+ lines.push(' const node = clone.firstChild;');
755
+
756
+ // Setup bindings per item
757
+ for (const b of forBlock.bindings) {
758
+ const nodeRef = pathExpr(b.path, 'node');
759
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
760
+ // Static binding: reference only item/index, assign once
761
+ lines.push(` ${nodeRef}.textContent = ${b.name} ?? '';`);
762
+ } else {
763
+ // Reactive binding: references component variables, wrap in effect
764
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
765
+ lines.push(` __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
766
+ }
767
+ }
768
+
769
+ // Setup events per item
770
+ for (const e of forBlock.events) {
771
+ const nodeRef = pathExpr(e.path, 'node');
772
+ lines.push(` ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
773
+ }
774
+
775
+ // Setup show per item
776
+ for (const sb of forBlock.showBindings) {
777
+ const nodeRef = pathExpr(sb.path, 'node');
778
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
779
+ const expr = sb.expression;
780
+ lines.push(` ${nodeRef}.style.display = (${expr}) ? '' : 'none';`);
781
+ } else {
782
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
783
+ lines.push(` __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
784
+ }
785
+ }
786
+
787
+ // Setup attr bindings per item
788
+ for (const ab of forBlock.attrBindings) {
789
+ const nodeRef = pathExpr(ab.path, 'node');
790
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
791
+ const expr = ab.expression;
792
+ lines.push(` const __val_${ab.varName} = ${expr};`);
793
+ lines.push(` if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
794
+ } else {
795
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
796
+ lines.push(` __effect(() => {`);
797
+ lines.push(` const __val = ${expr};`);
798
+ lines.push(` if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
799
+ lines.push(` else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
800
+ lines.push(` });`);
801
+ }
802
+ }
803
+
804
+ // Setup model bindings per item
805
+ for (const mb of (forBlock.modelBindings || [])) {
806
+ const nodeRef = pathExpr(mb.path, 'node');
807
+ // Effect (signal → DOM) — always reactive since it references component variables
808
+ lines.push(` __effect(() => {`);
809
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
810
+ lines.push(` ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
811
+ } else if (mb.prop === 'checked') {
812
+ lines.push(` ${nodeRef}.checked = !!this._${mb.signal}();`);
813
+ } else {
814
+ lines.push(` ${nodeRef}.value = this._${mb.signal}() ?? '';`);
815
+ }
816
+ lines.push(` });`);
817
+ // Listener (DOM → signal)
818
+ if (mb.prop === 'checked' && mb.radioValue === null) {
819
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
820
+ } else if (mb.coerce) {
821
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
822
+ } else {
823
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
824
+ }
825
+ }
826
+
827
+ // Setup scoped slot resolution per item
828
+ for (const s of (forBlock.slots || [])) {
829
+ if (s.slotProps.length > 0) {
830
+ const slotNodeRef = pathExpr(s.path, 'node');
831
+ const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
832
+ lines.push(` { const __slotEl = ${slotNodeRef};`);
833
+ lines.push(` const __sp = { ${propsEntries} };`);
834
+ lines.push(` let __h = __slotEl.innerHTML;`);
835
+ lines.push(` for (const [k, v] of Object.entries(__sp)) {`);
836
+ lines.push(` __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
837
+ lines.push(` }`);
838
+ lines.push(` __slotEl.innerHTML = __h;`);
839
+ lines.push(` }`);
840
+ }
841
+ }
842
+
843
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
844
+ lines.push(` this.${vn}_nodes.push(node);`);
845
+ lines.push(' });');
846
+ lines.push(' });');
847
+ }
848
+
849
+ // Lifecycle: onMount hooks (at the very end of connectedCallback)
850
+ for (const hook of onMountHooks) {
851
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
852
+ const bodyLines = body.split('\n');
853
+ for (const line of bodyLines) {
854
+ lines.push(` ${line}`);
855
+ }
856
+ }
857
+
204
858
  lines.push(' }');
205
859
  lines.push('');
206
860
 
207
- // Public getters/setters (task 6.2)
208
- for (const p of props) {
209
- lines.push(` get ${p}() { return this._s_${p}(); }`);
210
- lines.push(` set ${p}(val) { this._s_${p}(val); }`);
861
+ // disconnectedCallback (only when destroy hooks exist)
862
+ if (onDestroyHooks.length > 0) {
863
+ lines.push(' disconnectedCallback() {');
864
+ for (const hook of onDestroyHooks) {
865
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
866
+ const bodyLines = body.split('\n');
867
+ for (const line of bodyLines) {
868
+ lines.push(` ${line}`);
869
+ }
870
+ }
871
+ lines.push(' }');
211
872
  lines.push('');
212
873
  }
213
874
 
214
- // _emit method (task 6.4)
215
- if (events.length > 0 || hasEmitCall(methods)) {
875
+ // attributeChangedCallback (if props exist)
876
+ if (propDefs.length > 0) {
877
+ lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
878
+ for (const p of propDefs) {
879
+ const defaultVal = p.default;
880
+ let updateExpr;
881
+
882
+ if (defaultVal === 'true' || defaultVal === 'false') {
883
+ // Boolean coercion: attribute presence = true
884
+ updateExpr = `this._s_${p.name}(newVal != null)`;
885
+ } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
886
+ // Number coercion
887
+ updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
888
+ } else if (defaultVal === 'undefined') {
889
+ // Undefined default — pass through
890
+ updateExpr = `this._s_${p.name}(newVal)`;
891
+ } else {
892
+ // String default — use nullish coalescing
893
+ updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
894
+ }
895
+
896
+ lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
897
+ }
898
+ lines.push(' }');
899
+ lines.push('');
900
+
901
+ // Public getters and setters
902
+ for (const p of propDefs) {
903
+ lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
904
+ lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
905
+ lines.push('');
906
+ }
907
+ }
908
+
909
+ // _emit method (if emits declared)
910
+ if (emits.length > 0) {
216
911
  lines.push(' _emit(name, detail) {');
217
912
  lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
218
913
  lines.push(' }');
219
914
  lines.push('');
220
915
  }
221
916
 
222
- // User methods (task 6.4 — transform emit, variable refs)
917
+ // User methods (prefixed with _)
223
918
  for (const m of methods) {
224
- let body = transformMethodBody(m.body, propsSet, rootVarNames, computedNames);
919
+ const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
225
920
  lines.push(` _${m.name}(${m.params}) {`);
226
- lines.push(` ${body}`);
921
+ const bodyLines = body.split('\n');
922
+ for (const line of bodyLines) {
923
+ lines.push(` ${line}`);
924
+ }
227
925
  lines.push(' }');
228
926
  lines.push('');
229
927
  }
230
928
 
231
- lines.push('}');
232
- lines.push('');
233
-
234
- // customElements.define (task 6.1)
235
- lines.push(`customElements.define('${tagName}', ${className});`);
236
- lines.push('');
929
+ // ── Ref getter properties ──
930
+ for (const rd of refs) {
931
+ // Find matching RefBinding
932
+ const rb = refBindings.find(b => b.refName === rd.refName);
933
+ if (rb) {
934
+ lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
935
+ lines.push('');
936
+ }
937
+ }
237
938
 
238
- return lines.join('\n');
239
- }
939
+ // ── if setup methods ──
940
+ for (const ifBlock of ifBlocks) {
941
+ const vn = ifBlock.varName;
942
+ const hasSetup = ifBlock.branches.some(b =>
943
+ (b.bindings && b.bindings.length > 0) ||
944
+ (b.events && b.events.length > 0) ||
945
+ (b.showBindings && b.showBindings.length > 0) ||
946
+ (b.attrBindings && b.attrBindings.length > 0) ||
947
+ (b.modelBindings && b.modelBindings.length > 0)
948
+ );
949
+ if (!hasSetup) continue;
240
950
 
241
- // ── Helpers ──────────────────────────────────────────────────────────
951
+ lines.push(` ${vn}_setup(node, branch) {`);
952
+ for (let i = 0; i < ifBlock.branches.length; i++) {
953
+ const branch = ifBlock.branches[i];
954
+ const hasBranchSetup =
955
+ (branch.bindings && branch.bindings.length > 0) ||
956
+ (branch.events && branch.events.length > 0) ||
957
+ (branch.showBindings && branch.showBindings.length > 0) ||
958
+ (branch.attrBindings && branch.attrBindings.length > 0) ||
959
+ (branch.modelBindings && branch.modelBindings.length > 0);
960
+ if (!hasBranchSetup) continue;
242
961
 
243
- /**
244
- * Transform an expression by replacing bare variable references with signal calls.
245
- * Props → this._s_name(), reactive vars → this._name(), computeds → this._c_name()
246
- */
247
- function transformExpr(expr, propsSet, rootVarNames, computedNames) {
248
- let r = expr;
249
- for (const p of propsSet) {
250
- r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
251
- }
252
- for (const n of rootVarNames) {
253
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
254
- }
255
- for (const n of computedNames) {
256
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
257
- }
258
- return r;
259
- }
962
+ const keyword = i === 0 ? 'if' : 'else if';
963
+ lines.push(` ${keyword} (branch === ${i}) {`);
260
964
 
261
- /**
262
- * Transform a method body: replace variable refs, assignments, and emit calls.
263
- */
264
- function transformMethodBody(body, propsSet, rootVarNames, computedNames) {
265
- let r = body;
965
+ // Bindings: generate DOM refs and effects for text bindings
966
+ for (const b of branch.bindings) {
967
+ lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
968
+ if (b.type === 'prop') {
969
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
970
+ } else if (b.type === 'signal') {
971
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
972
+ } else if (b.type === 'computed') {
973
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
974
+ } else {
975
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
976
+ }
977
+ }
266
978
 
267
- // Replace assignments: `const varName = expr;` or `varName = expr;` → `this._varName(expr);`
268
- for (const n of rootVarNames) {
269
- r = r.replace(new RegExp(`(?:const|let|var)\\s+${n}\\s*=\\s*(.+?);?\\s*$`, 'gm'), `this._${n}($1);`);
270
- r = r.replace(new RegExp(`(?<!\\.)\\b${n}\\s*=\\s*(.+?);?\\s*$`, 'gm'), `this._${n}($1);`);
271
- }
979
+ // Events: generate addEventListener
980
+ for (const e of branch.events) {
981
+ lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
982
+ lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
983
+ }
272
984
 
273
- // Replace reads (not followed by = or preceded by this._)
274
- for (const n of rootVarNames) {
275
- r = r.replace(new RegExp(`(?<!this\\._)(?<!\\.)\\b${n}\\b(?!\\s*[=(])`, 'g'), `this._${n}()`);
276
- }
277
- for (const p of propsSet) {
278
- r = r.replace(new RegExp(`(?<!this\\._s_)(?<!\\.)\\b${p}\\b(?!\\s*[=(])`, 'g'), `this._s_${p}()`);
279
- }
280
- for (const n of computedNames) {
281
- r = r.replace(new RegExp(`(?<!this\\._c_)(?<!\\.)\\b${n}\\b`, 'g'), `this._c_${n}()`);
282
- }
985
+ // Show bindings: generate effects
986
+ for (const sb of branch.showBindings) {
987
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
988
+ lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
989
+ lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
990
+ }
283
991
 
284
- // Replace emit() this._emit()
285
- r = r.replace(/\bemit\(/g, 'this._emit(');
992
+ // Attr bindings: generate effects
993
+ for (const ab of branch.attrBindings) {
994
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
995
+ lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
996
+ lines.push(` __effect(() => {`);
997
+ lines.push(` const __val = ${expr};`);
998
+ lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
999
+ lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
1000
+ lines.push(` });`);
1001
+ }
286
1002
 
287
- return r;
288
- }
1003
+ // Model bindings: generate effects and listeners
1004
+ for (const mb of (branch.modelBindings || [])) {
1005
+ const nodeRef = pathExpr(mb.path, 'node');
1006
+ lines.push(` const ${mb.varName} = ${nodeRef};`);
1007
+ // Effect (signal → DOM)
1008
+ lines.push(` __effect(() => {`);
1009
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
1010
+ lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
1011
+ } else if (mb.prop === 'checked') {
1012
+ lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
1013
+ } else {
1014
+ lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
1015
+ }
1016
+ lines.push(` });`);
1017
+ // Listener (DOM → signal)
1018
+ if (mb.prop === 'checked' && mb.radioValue === null) {
1019
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
1020
+ } else if (mb.coerce) {
1021
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
1022
+ } else {
1023
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
1024
+ }
1025
+ }
289
1026
 
290
- /**
291
- * Get the signal reference expression for a binding.
292
- */
293
- function bindingRef(b) {
294
- if (b.type === 'prop') return `this._s_${b.name}()`;
295
- if (b.type === 'computed') return `this._c_${b.name}()`;
296
- return `this._${b.name}()`;
297
- }
1027
+ lines.push(' }');
1028
+ }
1029
+ lines.push(' }');
1030
+ lines.push('');
1031
+ }
298
1032
 
299
- /**
300
- * Get the signal reference for a watcher target or slot prop source.
301
- */
302
- function signalRef(name, propsSet, computedNames, rootVarNames) {
303
- if (propsSet.has(name)) return `this._s_${name}()`;
304
- if (computedNames.has(name)) return `this._c_${name}()`;
305
- if (rootVarNames.has(name)) return `this._${name}()`;
306
- return `this._${name}()`;
307
- }
1033
+ lines.push('}');
1034
+ lines.push('');
308
1035
 
309
- /**
310
- * Get the signal reference for a slot prop source.
311
- */
312
- function slotPropRef(source, propsSet, computedNames, rootVarNames) {
313
- if (propsSet.has(source)) return `this._s_${source}()`;
314
- if (computedNames.has(source)) return `this._c_${source}()`;
315
- if (rootVarNames.has(source)) return `this._${source}()`;
316
- return `'${source}'`;
317
- }
1036
+ // ── 5. Custom element registration ──
1037
+ lines.push(`customElements.define('${tagName}', ${className});`);
318
1038
 
319
- /**
320
- * Check if any method body contains an emit() call.
321
- */
322
- function hasEmitCall(methods) {
323
- return methods.some(m => /\bemit\(/.test(m.body));
1039
+ return lines.join('\n');
324
1040
  }