@sprlab/wccompiler 0.0.2 → 0.2.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,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,43 @@ 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 = [],
43
325
  } = parseResult;
44
326
 
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));
327
+ const signalNames = signals.map(s => s.name);
328
+ const computedNames = computeds.map(c => c.name);
329
+ const constantNames = constantVars.map(v => v.name);
330
+ const refVarNames = refs.map(r => r.varName);
331
+ const propNames = new Set(propDefs.map(p => p.name));
48
332
 
49
333
  const lines = [];
50
334
 
51
- // ── 1. Inline reactive runtime (task 6.1) ──
335
+ // ── 1. Inline reactive runtime ──
52
336
  lines.push(reactiveRuntime.trim());
53
337
  lines.push('');
54
338
 
55
- // ── 2. CSS injection (task 6.6) ──
339
+ // ── 2. CSS injection (scoped CSS into document.head, always) ──
56
340
  if (style) {
57
341
  const scoped = scopeCSS(style, tagName);
58
342
  lines.push(`const __css_${className} = document.createElement('style');`);
@@ -61,25 +345,26 @@ export function generateComponent(parseResult) {
61
345
  lines.push('');
62
346
  }
63
347
 
64
- // ── 3. Template ──
348
+ // ── 3. Template element ──
65
349
  lines.push(`const __t_${className} = document.createElement('template');`);
66
- lines.push(`__t_${className}.innerHTML = \`${processedTemplate}\`;`);
350
+ lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
67
351
  lines.push('');
68
352
 
69
- // ── 4. Class definition (task 6.1) ──
353
+ // ── 4. HTMLElement class ──
70
354
  lines.push(`class ${className} extends HTMLElement {`);
71
355
 
72
- // observedAttributes
73
- lines.push(' static get observedAttributes() {');
74
- lines.push(` return [${props.map(p => `'${p}'`).join(', ')}];`);
75
- lines.push(' }');
76
- lines.push('');
356
+ // Static observedAttributes (if props exist)
357
+ if (propDefs.length > 0) {
358
+ const attrNames = propDefs.map(p => `'${p.attrName}'`).join(', ');
359
+ lines.push(` static get observedAttributes() { return [${attrNames}]; }`);
360
+ lines.push('');
361
+ }
77
362
 
78
- // constructor
363
+ // Constructor
79
364
  lines.push(' constructor() {');
80
365
  lines.push(' super();');
81
366
 
82
- // Slot resolution code (task 6.5) must read childNodes BEFORE replacing innerHTML
367
+ // Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
83
368
  if (slots.length > 0) {
84
369
  lines.push(' const __slotMap = {};');
85
370
  lines.push(' const __defaultSlotNodes = [];');
@@ -97,48 +382,116 @@ export function generateComponent(parseResult) {
97
382
  lines.push(' }');
98
383
  }
99
384
 
100
- // Clone template and assign DOM refs
385
+ // Clone template
101
386
  lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
102
387
 
103
- const allNodes = [...bindings, ...events, ...slots];
104
- for (const n of allNodes) {
105
- lines.push(` this.${n.varName} = ${pathExpr(n.path, '__root')};`);
388
+ // Assign DOM refs for bindings
389
+ for (const b of bindings) {
390
+ lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
106
391
  }
107
392
 
108
- lines.push(" this.innerHTML = '';");
109
- lines.push(' this.appendChild(__root);');
393
+ // Assign DOM refs for events
394
+ for (const e of events) {
395
+ lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
396
+ }
397
+
398
+ // Assign DOM refs for show bindings
399
+ for (const sb of showBindings) {
400
+ lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
401
+ }
110
402
 
111
- // Static slot injection (task 6.5)
403
+ // Assign DOM refs for model bindings
404
+ for (const mb of modelBindings) {
405
+ lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
406
+ }
407
+
408
+ // Assign DOM refs for slot placeholders
112
409
  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; }`);
410
+ lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
411
+ }
412
+
413
+ // Assign DOM refs for attr bindings (reuse ref when same path)
414
+ const attrPathMap = new Map();
415
+ for (const ab of attrBindings) {
416
+ const pathKey = ab.path.join('.');
417
+ if (attrPathMap.has(pathKey)) {
418
+ lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
119
419
  } else {
120
- // Default slot
121
- lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
420
+ lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
421
+ attrPathMap.set(pathKey, ab.varName);
122
422
  }
123
423
  }
124
424
 
125
- // Signal inits (task 6.2)
126
- for (const p of props) {
127
- lines.push(` this._s_${p} = __signal(null);`);
425
+ // Prop signal initialization (BEFORE user signals)
426
+ for (const p of propDefs) {
427
+ lines.push(` this._s_${p.name} = __signal(${p.default});`);
128
428
  }
129
- for (const v of reactiveVars) {
130
- lines.push(` this._${v.name} = __signal(${v.value});`);
429
+
430
+ // Signal initialization
431
+ for (const s of signals) {
432
+ lines.push(` this._${s.name} = __signal(${s.value});`);
131
433
  }
132
434
 
133
- // Computed inits (task 6.2)
435
+ // Constant initialization
436
+ for (const c of constantVars) {
437
+ lines.push(` this._const_${c.name} = ${c.value};`);
438
+ }
439
+
440
+ // Computed initialization
134
441
  for (const c of computeds) {
135
- const body = transformExpr(c.body, propsSet, rootVarNames, computedNames);
442
+ const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
136
443
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
137
444
  }
138
445
 
139
- // Watcher prev inits (task 6.3)
140
- for (const w of watchers) {
141
- lines.push(` this.__prev_${w.target} = undefined;`);
446
+ // ── if: template creation, anchor reference, state init ──
447
+ for (const ifBlock of ifBlocks) {
448
+ const vn = ifBlock.varName;
449
+ // Template per branch
450
+ for (let i = 0; i < ifBlock.branches.length; i++) {
451
+ const branch = ifBlock.branches[i];
452
+ lines.push(` this.${vn}_t${i} = document.createElement('template');`);
453
+ lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
454
+ }
455
+ // Reference to anchor comment node (must be before appendChild moves nodes out of __root)
456
+ lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
457
+ // Active branch state
458
+ lines.push(` this.${vn}_current = null;`);
459
+ lines.push(` this.${vn}_active = undefined;`);
460
+ }
461
+
462
+ // ── each: template creation, anchor reference, nodes array ──
463
+ for (const forBlock of forBlocks) {
464
+ const vn = forBlock.varName;
465
+ lines.push(` this.${vn}_tpl = document.createElement('template');`);
466
+ lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
467
+ lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
468
+ lines.push(` this.${vn}_nodes = [];`);
469
+ }
470
+
471
+ // ── Ref DOM reference assignments (before appendChild moves nodes) ──
472
+ for (const rb of refBindings) {
473
+ lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
474
+ }
475
+
476
+ // Append DOM (always light DOM)
477
+ lines.push(" this.innerHTML = '';");
478
+ lines.push(' this.appendChild(__root);');
479
+
480
+ // Static slot injection (after DOM is appended)
481
+ for (const s of slots) {
482
+ if (s.name && s.slotProps.length > 0) {
483
+ // Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
484
+ lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
485
+ if (s.defaultContent) {
486
+ lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
487
+ }
488
+ } else if (s.name) {
489
+ // Named slot: inject content directly
490
+ lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
491
+ } else {
492
+ // Default slot: inject collected child nodes
493
+ lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
494
+ }
142
495
  }
143
496
 
144
497
  lines.push(' }');
@@ -147,19 +500,35 @@ export function generateComponent(parseResult) {
147
500
  // connectedCallback
148
501
  lines.push(' connectedCallback() {');
149
502
 
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)} ?? '';`);
503
+ // Binding effects one __effect per binding
504
+ for (const b of bindings) {
505
+ if (b.type === 'prop') {
506
+ lines.push(' __effect(() => {');
507
+ lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
508
+ lines.push(' });');
509
+ } else if (b.type === 'signal') {
510
+ lines.push(' __effect(() => {');
511
+ lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
512
+ lines.push(' });');
513
+ } else if (b.type === 'computed') {
514
+ lines.push(' __effect(() => {');
515
+ lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
516
+ lines.push(' });');
517
+ } else {
518
+ // method type — call the method
519
+ lines.push(' __effect(() => {');
520
+ lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
521
+ lines.push(' });');
155
522
  }
156
- lines.push(' });');
157
523
  }
158
524
 
159
- // Reactive slot effects (task 6.5)
525
+ // Scoped slot effects reactive resolution of {{propName}} in consumer templates
160
526
  for (const s of slots) {
161
527
  if (s.name && s.slotProps.length > 0) {
162
- const propsObj = s.slotProps.map(sp => `${sp.prop}: ${slotPropRef(sp.source, propsSet, computedNames, rootVarNames)}`).join(', ');
528
+ const propsObj = s.slotProps.map(sp => {
529
+ const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
530
+ return `${sp.prop}: ${ref}`;
531
+ }).join(', ');
163
532
  lines.push(` if (this.__slotTpl_${s.name}) {`);
164
533
  lines.push(' __effect(() => {');
165
534
  lines.push(` const __props = { ${propsObj} };`);
@@ -173,152 +542,463 @@ export function generateComponent(parseResult) {
173
542
  }
174
543
  }
175
544
 
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(');
545
+ // User effects
546
+ for (const eff of effects) {
547
+ const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
181
548
  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};`);
549
+ // Indent each line of the body
550
+ const bodyLines = body.split('\n');
551
+ for (const line of bodyLines) {
552
+ lines.push(` ${line}`);
553
+ }
188
554
  lines.push(' });');
189
555
  }
190
556
 
191
- // Event listeners (task 6.4)
557
+ // Event listeners
192
558
  for (const e of events) {
193
559
  lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
194
560
  }
195
561
 
196
- lines.push(' }');
197
- lines.push('');
562
+ // Show effects — one __effect per ShowBinding
563
+ for (const sb of showBindings) {
564
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
565
+ lines.push(' __effect(() => {');
566
+ lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
567
+ lines.push(' });');
568
+ }
198
569
 
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);`);
570
+ // Model effects — signal → DOM (one __effect per ModelBinding)
571
+ for (const mb of modelBindings) {
572
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
573
+ // Radio: compare signal value to radioValue
574
+ lines.push(' __effect(() => {');
575
+ lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
576
+ lines.push(' });');
577
+ } else if (mb.prop === 'checked') {
578
+ // Checkbox: coerce to boolean
579
+ lines.push(' __effect(() => {');
580
+ lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
581
+ lines.push(' });');
582
+ } else {
583
+ // Value-based (text, number, textarea, select): nullish coalesce to ''
584
+ lines.push(' __effect(() => {');
585
+ lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
586
+ lines.push(' });');
587
+ }
203
588
  }
204
- lines.push(' }');
205
- lines.push('');
206
589
 
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); }`);
211
- lines.push('');
590
+ // Model event listeners — DOM → signal (one addEventListener per ModelBinding)
591
+ for (const mb of modelBindings) {
592
+ if (mb.prop === 'checked' && mb.radioValue === null) {
593
+ // Checkbox: read e.target.checked
594
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
595
+ } else if (mb.coerce) {
596
+ // Number input: wrap in Number()
597
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
598
+ } else {
599
+ // All others: read e.target.value
600
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
601
+ }
212
602
  }
213
603
 
214
- // _emit method (task 6.4)
215
- if (events.length > 0 || hasEmitCall(methods)) {
216
- lines.push(' _emit(name, detail) {');
217
- lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
218
- lines.push(' }');
219
- lines.push('');
604
+ // Attr binding effects — one __effect per AttrBinding
605
+ for (const ab of attrBindings) {
606
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
607
+ if (ab.kind === 'attr') {
608
+ lines.push(' __effect(() => {');
609
+ lines.push(` const __v = ${expr};`);
610
+ lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
611
+ lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
612
+ lines.push(' });');
613
+ } else if (ab.kind === 'bool') {
614
+ lines.push(' __effect(() => {');
615
+ lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
616
+ lines.push(' });');
617
+ } else if (ab.kind === 'class') {
618
+ if (ab.expression.trimStart().startsWith('{')) {
619
+ // Object expression: iterate entries, classList.add/remove
620
+ lines.push(' __effect(() => {');
621
+ lines.push(` const __obj = ${expr};`);
622
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
623
+ lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
624
+ lines.push(' }');
625
+ lines.push(' });');
626
+ } else {
627
+ // String expression: set className
628
+ lines.push(' __effect(() => {');
629
+ lines.push(` this.${ab.varName}.className = ${expr};`);
630
+ lines.push(' });');
631
+ }
632
+ } else if (ab.kind === 'style') {
633
+ if (ab.expression.trimStart().startsWith('{')) {
634
+ // Object expression: iterate entries, set style[key]
635
+ lines.push(' __effect(() => {');
636
+ lines.push(` const __obj = ${expr};`);
637
+ lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
638
+ lines.push(` this.${ab.varName}.style[__k] = __val;`);
639
+ lines.push(' }');
640
+ lines.push(' });');
641
+ } else {
642
+ // String expression: set cssText
643
+ lines.push(' __effect(() => {');
644
+ lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
645
+ lines.push(' });');
646
+ }
647
+ }
220
648
  }
221
649
 
222
- // User methods (task 6.4 — transform emit, variable refs)
223
- for (const m of methods) {
224
- let body = transformMethodBody(m.body, propsSet, rootVarNames, computedNames);
225
- lines.push(` _${m.name}(${m.params}) {`);
226
- lines.push(` ${body}`);
227
- lines.push(' }');
228
- lines.push('');
650
+ // ── if effects ──
651
+ for (const ifBlock of ifBlocks) {
652
+ const vn = ifBlock.varName;
653
+ lines.push(' __effect(() => {');
654
+ lines.push(' let __branch = null;');
655
+ for (let i = 0; i < ifBlock.branches.length; i++) {
656
+ const branch = ifBlock.branches[i];
657
+ if (branch.type === 'if') {
658
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
659
+ lines.push(` if (${expr}) { __branch = ${i}; }`);
660
+ } else if (branch.type === 'else-if') {
661
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
662
+ lines.push(` else if (${expr}) { __branch = ${i}; }`);
663
+ } else {
664
+ // else
665
+ lines.push(` else { __branch = ${i}; }`);
666
+ }
667
+ }
668
+ lines.push(` if (__branch === this.${vn}_active) return;`);
669
+ // Remove previous branch
670
+ lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
671
+ // Insert new branch
672
+ lines.push(' if (__branch !== null) {');
673
+ const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
674
+ lines.push(` const tpl = [${tplArray}][__branch];`);
675
+ lines.push(' const clone = tpl.content.cloneNode(true);');
676
+ lines.push(' const node = clone.firstChild;');
677
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
678
+ lines.push(` this.${vn}_current = node;`);
679
+ // Setup bindings/events for active branch (only if any branch has bindings/events)
680
+ const hasSetup = ifBlock.branches.some(b =>
681
+ (b.bindings && b.bindings.length > 0) ||
682
+ (b.events && b.events.length > 0) ||
683
+ (b.showBindings && b.showBindings.length > 0) ||
684
+ (b.attrBindings && b.attrBindings.length > 0) ||
685
+ (b.modelBindings && b.modelBindings.length > 0)
686
+ );
687
+ if (hasSetup) {
688
+ lines.push(` this.${vn}_setup(node, __branch);`);
689
+ }
690
+ lines.push(' }');
691
+ lines.push(` this.${vn}_active = __branch;`);
692
+ lines.push(' });');
229
693
  }
230
694
 
231
- lines.push('}');
232
- lines.push('');
695
+ // ── each effects ──
696
+ for (const forBlock of forBlocks) {
697
+ const vn = forBlock.varName;
698
+ const { itemVar, indexVar, source } = forBlock;
233
699
 
234
- // customElements.define (task 6.1)
235
- lines.push(`customElements.define('${tagName}', ${className});`);
236
- lines.push('');
700
+ const signalNamesSet = new Set(signalNames);
701
+ const computedNamesSet = new Set(computedNames);
237
702
 
238
- return lines.join('\n');
239
- }
703
+ // Transform the source expression
704
+ const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
240
705
 
241
- // ── Helpers ──────────────────────────────────────────────────────────
706
+ lines.push(' __effect(() => {');
707
+ lines.push(` const __source = ${sourceExpr};`);
708
+ lines.push('');
709
+ lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
710
+ lines.push(` this.${vn}_nodes = [];`);
711
+ lines.push('');
712
+ lines.push(" const __iter = typeof __source === 'number'");
713
+ lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
714
+ lines.push(' : (__source || []);');
715
+ lines.push('');
716
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
717
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
718
+ lines.push(' const node = clone.firstChild;');
719
+
720
+ // Setup bindings per item
721
+ for (const b of forBlock.bindings) {
722
+ const nodeRef = pathExpr(b.path, 'node');
723
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
724
+ // Static binding: reference only item/index, assign once
725
+ lines.push(` ${nodeRef}.textContent = ${b.name} ?? '';`);
726
+ } else {
727
+ // Reactive binding: references component variables, wrap in effect
728
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
729
+ lines.push(` __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
730
+ }
731
+ }
242
732
 
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}()`);
733
+ // Setup events per item
734
+ for (const e of forBlock.events) {
735
+ const nodeRef = pathExpr(e.path, 'node');
736
+ lines.push(` ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
737
+ }
738
+
739
+ // Setup show per item
740
+ for (const sb of forBlock.showBindings) {
741
+ const nodeRef = pathExpr(sb.path, 'node');
742
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
743
+ const expr = sb.expression;
744
+ lines.push(` ${nodeRef}.style.display = (${expr}) ? '' : 'none';`);
745
+ } else {
746
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
747
+ lines.push(` __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
748
+ }
749
+ }
750
+
751
+ // Setup attr bindings per item
752
+ for (const ab of forBlock.attrBindings) {
753
+ const nodeRef = pathExpr(ab.path, 'node');
754
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
755
+ const expr = ab.expression;
756
+ lines.push(` const __val_${ab.varName} = ${expr};`);
757
+ lines.push(` if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
758
+ } else {
759
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
760
+ lines.push(` __effect(() => {`);
761
+ lines.push(` const __val = ${expr};`);
762
+ lines.push(` if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
763
+ lines.push(` else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
764
+ lines.push(` });`);
765
+ }
766
+ }
767
+
768
+ // Setup model bindings per item
769
+ for (const mb of (forBlock.modelBindings || [])) {
770
+ const nodeRef = pathExpr(mb.path, 'node');
771
+ // Effect (signal → DOM) — always reactive since it references component variables
772
+ lines.push(` __effect(() => {`);
773
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
774
+ lines.push(` ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
775
+ } else if (mb.prop === 'checked') {
776
+ lines.push(` ${nodeRef}.checked = !!this._${mb.signal}();`);
777
+ } else {
778
+ lines.push(` ${nodeRef}.value = this._${mb.signal}() ?? '';`);
779
+ }
780
+ lines.push(` });`);
781
+ // Listener (DOM → signal)
782
+ if (mb.prop === 'checked' && mb.radioValue === null) {
783
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
784
+ } else if (mb.coerce) {
785
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
786
+ } else {
787
+ lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
788
+ }
789
+ }
790
+
791
+ // Setup scoped slot resolution per item
792
+ for (const s of (forBlock.slots || [])) {
793
+ if (s.slotProps.length > 0) {
794
+ const slotNodeRef = pathExpr(s.path, 'node');
795
+ const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
796
+ lines.push(` { const __slotEl = ${slotNodeRef};`);
797
+ lines.push(` const __sp = { ${propsEntries} };`);
798
+ lines.push(` let __h = __slotEl.innerHTML;`);
799
+ lines.push(` for (const [k, v] of Object.entries(__sp)) {`);
800
+ lines.push(` __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
801
+ lines.push(` }`);
802
+ lines.push(` __slotEl.innerHTML = __h;`);
803
+ lines.push(` }`);
804
+ }
805
+ }
806
+
807
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
808
+ lines.push(` this.${vn}_nodes.push(node);`);
809
+ lines.push(' });');
810
+ lines.push(' });');
254
811
  }
255
- for (const n of computedNames) {
256
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
812
+
813
+ // Lifecycle: onMount hooks (at the very end of connectedCallback)
814
+ for (const hook of onMountHooks) {
815
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
816
+ const bodyLines = body.split('\n');
817
+ for (const line of bodyLines) {
818
+ lines.push(` ${line}`);
819
+ }
257
820
  }
258
- return r;
259
- }
260
821
 
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;
822
+ lines.push(' }');
823
+ lines.push('');
266
824
 
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);`);
825
+ // disconnectedCallback (only when destroy hooks exist)
826
+ if (onDestroyHooks.length > 0) {
827
+ lines.push(' disconnectedCallback() {');
828
+ for (const hook of onDestroyHooks) {
829
+ const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
830
+ const bodyLines = body.split('\n');
831
+ for (const line of bodyLines) {
832
+ lines.push(` ${line}`);
833
+ }
834
+ }
835
+ lines.push(' }');
836
+ lines.push('');
271
837
  }
272
838
 
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}()`);
839
+ // attributeChangedCallback (if props exist)
840
+ if (propDefs.length > 0) {
841
+ lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
842
+ for (const p of propDefs) {
843
+ const defaultVal = p.default;
844
+ let updateExpr;
845
+
846
+ if (defaultVal === 'true' || defaultVal === 'false') {
847
+ // Boolean coercion: attribute presence = true
848
+ updateExpr = `this._s_${p.name}(newVal != null)`;
849
+ } else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
850
+ // Number coercion
851
+ updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
852
+ } else if (defaultVal === 'undefined') {
853
+ // Undefined default — pass through
854
+ updateExpr = `this._s_${p.name}(newVal)`;
855
+ } else {
856
+ // String default — use nullish coalescing
857
+ updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
858
+ }
859
+
860
+ lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
861
+ }
862
+ lines.push(' }');
863
+ lines.push('');
864
+
865
+ // Public getters and setters
866
+ for (const p of propDefs) {
867
+ lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
868
+ lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
869
+ lines.push('');
870
+ }
276
871
  }
277
- for (const p of propsSet) {
278
- r = r.replace(new RegExp(`(?<!this\\._s_)(?<!\\.)\\b${p}\\b(?!\\s*[=(])`, 'g'), `this._s_${p}()`);
872
+
873
+ // _emit method (if emits declared)
874
+ if (emits.length > 0) {
875
+ lines.push(' _emit(name, detail) {');
876
+ lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
877
+ lines.push(' }');
878
+ lines.push('');
279
879
  }
280
- for (const n of computedNames) {
281
- r = r.replace(new RegExp(`(?<!this\\._c_)(?<!\\.)\\b${n}\\b`, 'g'), `this._c_${n}()`);
880
+
881
+ // User methods (prefixed with _)
882
+ for (const m of methods) {
883
+ const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
884
+ lines.push(` _${m.name}(${m.params}) {`);
885
+ const bodyLines = body.split('\n');
886
+ for (const line of bodyLines) {
887
+ lines.push(` ${line}`);
888
+ }
889
+ lines.push(' }');
890
+ lines.push('');
282
891
  }
283
892
 
284
- // Replace emit() this._emit()
285
- r = r.replace(/\bemit\(/g, 'this._emit(');
893
+ // ── Ref getter properties ──
894
+ for (const rd of refs) {
895
+ // Find matching RefBinding
896
+ const rb = refBindings.find(b => b.refName === rd.refName);
897
+ if (rb) {
898
+ lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
899
+ lines.push('');
900
+ }
901
+ }
286
902
 
287
- return r;
288
- }
903
+ // ── if setup methods ──
904
+ for (const ifBlock of ifBlocks) {
905
+ const vn = ifBlock.varName;
906
+ const hasSetup = ifBlock.branches.some(b =>
907
+ (b.bindings && b.bindings.length > 0) ||
908
+ (b.events && b.events.length > 0) ||
909
+ (b.showBindings && b.showBindings.length > 0) ||
910
+ (b.attrBindings && b.attrBindings.length > 0) ||
911
+ (b.modelBindings && b.modelBindings.length > 0)
912
+ );
913
+ if (!hasSetup) continue;
914
+
915
+ lines.push(` ${vn}_setup(node, branch) {`);
916
+ for (let i = 0; i < ifBlock.branches.length; i++) {
917
+ const branch = ifBlock.branches[i];
918
+ const hasBranchSetup =
919
+ (branch.bindings && branch.bindings.length > 0) ||
920
+ (branch.events && branch.events.length > 0) ||
921
+ (branch.showBindings && branch.showBindings.length > 0) ||
922
+ (branch.attrBindings && branch.attrBindings.length > 0) ||
923
+ (branch.modelBindings && branch.modelBindings.length > 0);
924
+ if (!hasBranchSetup) continue;
925
+
926
+ const keyword = i === 0 ? 'if' : 'else if';
927
+ lines.push(` ${keyword} (branch === ${i}) {`);
928
+
929
+ // Bindings: generate DOM refs and effects for text bindings
930
+ for (const b of branch.bindings) {
931
+ lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
932
+ if (b.type === 'prop') {
933
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
934
+ } else if (b.type === 'signal') {
935
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
936
+ } else if (b.type === 'computed') {
937
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
938
+ } else {
939
+ lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
940
+ }
941
+ }
942
+
943
+ // Events: generate addEventListener
944
+ for (const e of branch.events) {
945
+ lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
946
+ lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
947
+ }
948
+
949
+ // Show bindings: generate effects
950
+ for (const sb of branch.showBindings) {
951
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
952
+ lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
953
+ lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
954
+ }
955
+
956
+ // Attr bindings: generate effects
957
+ for (const ab of branch.attrBindings) {
958
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
959
+ lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
960
+ lines.push(` __effect(() => {`);
961
+ lines.push(` const __val = ${expr};`);
962
+ lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
963
+ lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
964
+ lines.push(` });`);
965
+ }
966
+
967
+ // Model bindings: generate effects and listeners
968
+ for (const mb of (branch.modelBindings || [])) {
969
+ const nodeRef = pathExpr(mb.path, 'node');
970
+ lines.push(` const ${mb.varName} = ${nodeRef};`);
971
+ // Effect (signal → DOM)
972
+ lines.push(` __effect(() => {`);
973
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
974
+ lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
975
+ } else if (mb.prop === 'checked') {
976
+ lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
977
+ } else {
978
+ lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
979
+ }
980
+ lines.push(` });`);
981
+ // Listener (DOM → signal)
982
+ if (mb.prop === 'checked' && mb.radioValue === null) {
983
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
984
+ } else if (mb.coerce) {
985
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
986
+ } else {
987
+ lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
988
+ }
989
+ }
289
990
 
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
- }
991
+ lines.push(' }');
992
+ }
993
+ lines.push(' }');
994
+ lines.push('');
995
+ }
298
996
 
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
- }
997
+ lines.push('}');
998
+ lines.push('');
308
999
 
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
- }
1000
+ // ── 5. Custom element registration ──
1001
+ lines.push(`customElements.define('${tagName}', ${className});`);
318
1002
 
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));
1003
+ return lines.join('\n');
324
1004
  }