@sprlab/wccompiler 0.2.1 → 0.4.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
@@ -75,7 +75,7 @@ function slotPropRef(source, signalNames, computedNames, propNames) {
75
75
  * @param {string|null} [emitsObjectName] — Emits object variable name
76
76
  * @returns {string}
77
77
  */
78
- export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = []) {
78
+ export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = []) {
79
79
  let result = expr;
80
80
 
81
81
  // Transform emit calls: emitsObjectName( → this._emit(
@@ -84,6 +84,14 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
84
84
  result = result.replace(emitsRe, 'this._emit(');
85
85
  }
86
86
 
87
+ // Transform method calls: methodName( → this._methodName(
88
+ for (const name of methodNames) {
89
+ if (propsObjectName && name === propsObjectName) continue;
90
+ if (emitsObjectName && name === emitsObjectName) continue;
91
+ const methodRe = new RegExp(`\\b${name}\\(`, 'g');
92
+ result = result.replace(methodRe, `this._${name}(`);
93
+ }
94
+
87
95
  // Transform props.x → this._s_x() BEFORE signal/computed transforms
88
96
  if (propsObjectName && propNames.size > 0) {
89
97
  const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
@@ -95,6 +103,14 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
95
103
  });
96
104
  }
97
105
 
106
+ // Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
107
+ for (const propName of propNames) {
108
+ if (propsObjectName && propName === propsObjectName) continue;
109
+ if (emitsObjectName && propName === emitsObjectName) continue;
110
+ const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
111
+ result = result.replace(bareRe, `this._s_${propName}()`);
112
+ }
113
+
98
114
  // Transform computed names first (to avoid partial matches with signals)
99
115
  for (const name of computedNames) {
100
116
  // Skip propsObjectName and emitsObjectName
@@ -231,15 +247,24 @@ export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames
231
247
 
232
248
  for (const p of propsSet) {
233
249
  if (excludeSet.has(p)) continue;
234
- r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
250
+ // First: transform name() calls → this._s_name() (don't double-call)
251
+ r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
252
+ // Then: transform bare name references
253
+ r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
235
254
  }
236
255
  for (const n of rootVarNames) {
237
256
  if (excludeSet.has(n)) continue;
238
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
257
+ // First: transform name() calls → this._name() (don't double-call)
258
+ r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
259
+ // Then: transform bare name references
260
+ r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
239
261
  }
240
262
  for (const n of computedNames) {
241
263
  if (excludeSet.has(n)) continue;
242
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
264
+ // First: transform name() calls → this._c_name() (don't double-call)
265
+ r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
266
+ // Then: transform bare name references
267
+ r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
243
268
  }
244
269
  return r;
245
270
  }
@@ -289,6 +314,173 @@ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames,
289
314
  return true;
290
315
  }
291
316
 
317
+ /**
318
+ * Generate the JS expression for an event handler based on its type:
319
+ * - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
320
+ * - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
321
+ * - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
322
+ *
323
+ * @param {string} handler — The raw handler string from the template
324
+ * @param {string[]} signalNames
325
+ * @param {string[]} computedNames
326
+ * @param {string|null} propsObjectName
327
+ * @param {Set<string>} propNames
328
+ * @param {string|null} emitsObjectName
329
+ * @param {string[]} constantNames
330
+ * @returns {string}
331
+ */
332
+ export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames) {
333
+ if (handler.includes('=>')) {
334
+ // Arrow function expression: (e) => removeItem(item)
335
+ const arrowIdx = handler.indexOf('=>');
336
+ const params = handler.slice(0, arrowIdx).trim();
337
+ let body = handler.slice(arrowIdx + 2).trim();
338
+ body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames);
339
+ return `${params} => { ${body}; }`;
340
+ } else if (handler.includes('(')) {
341
+ // Function call expression: removeItem(item)
342
+ const parenIdx = handler.indexOf('(');
343
+ const fnName = handler.slice(0, parenIdx).trim();
344
+ const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
345
+ const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames) : '';
346
+ return `(e) => { this._${fnName}(${transformedArgs}); }`;
347
+ } else {
348
+ // Simple method name
349
+ return `this._${handler}.bind(this)`;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Generate the JS expression for an event handler inside an each block.
355
+ * Similar to generateEventHandler but uses transformForExpr for the each scope.
356
+ *
357
+ * @param {string} handler
358
+ * @param {string} itemVar
359
+ * @param {string|null} indexVar
360
+ * @param {Set<string>} propNames
361
+ * @param {Set<string>} signalNamesSet
362
+ * @param {Set<string>} computedNamesSet
363
+ * @returns {string}
364
+ */
365
+ export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
366
+ if (handler.includes('=>')) {
367
+ // Arrow function expression
368
+ const arrowIdx = handler.indexOf('=>');
369
+ const params = handler.slice(0, arrowIdx).trim();
370
+ let body = handler.slice(arrowIdx + 2).trim();
371
+ body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
372
+ return `${params} => { ${body}; }`;
373
+ } else if (handler.includes('(')) {
374
+ // Function call expression: removeItem(item)
375
+ const parenIdx = handler.indexOf('(');
376
+ const fnName = handler.slice(0, parenIdx).trim();
377
+ const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
378
+ const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
379
+ return `(e) => { this._${fnName}(${transformedArgs}); }`;
380
+ } else {
381
+ // Simple method name
382
+ return `this._${handler}.bind(this)`;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Generate per-item setup code for bindings, events, show, attr, model, and slots.
388
+ * Used by both keyed and non-keyed each effects.
389
+ *
390
+ * @param {string[]} lines — Output lines array
391
+ * @param {object} forBlock — ForBlock with bindings, events, etc.
392
+ * @param {string} itemVar
393
+ * @param {string|null} indexVar
394
+ * @param {Set<string>} propNames
395
+ * @param {Set<string>} signalNamesSet
396
+ * @param {Set<string>} computedNamesSet
397
+ */
398
+ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
399
+ const indent = ' ';
400
+
401
+ // Bindings
402
+ for (const b of forBlock.bindings) {
403
+ const nodeRef = pathExpr(b.path, 'node');
404
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
405
+ lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
406
+ } else {
407
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
408
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
409
+ }
410
+ }
411
+
412
+ // Events
413
+ for (const e of forBlock.events) {
414
+ const nodeRef = pathExpr(e.path, 'node');
415
+ const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
416
+ lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
417
+ }
418
+
419
+ // Show
420
+ for (const sb of forBlock.showBindings) {
421
+ const nodeRef = pathExpr(sb.path, 'node');
422
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
423
+ lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
424
+ } else {
425
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
426
+ lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
427
+ }
428
+ }
429
+
430
+ // Attr bindings
431
+ for (const ab of forBlock.attrBindings) {
432
+ const nodeRef = pathExpr(ab.path, 'node');
433
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
434
+ lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
435
+ lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
436
+ } else {
437
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
438
+ lines.push(`${indent} __effect(() => {`);
439
+ lines.push(`${indent} const __val = ${expr};`);
440
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
441
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
442
+ lines.push(`${indent} });`);
443
+ }
444
+ }
445
+
446
+ // Model bindings
447
+ for (const mb of (forBlock.modelBindings || [])) {
448
+ const nodeRef = pathExpr(mb.path, 'node');
449
+ lines.push(`${indent} __effect(() => {`);
450
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
451
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
452
+ } else if (mb.prop === 'checked') {
453
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
454
+ } else {
455
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
456
+ }
457
+ lines.push(`${indent} });`);
458
+ if (mb.prop === 'checked' && mb.radioValue === null) {
459
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
460
+ } else if (mb.coerce) {
461
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
462
+ } else {
463
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
464
+ }
465
+ }
466
+
467
+ // Scoped slots
468
+ for (const s of (forBlock.slots || [])) {
469
+ if (s.slotProps.length > 0) {
470
+ const slotNodeRef = pathExpr(s.path, 'node');
471
+ const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
472
+ lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
473
+ lines.push(`${indent} const __sp = { ${propsEntries} };`);
474
+ lines.push(`${indent} let __h = __slotEl.innerHTML;`);
475
+ lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
476
+ lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
477
+ lines.push(`${indent} }`);
478
+ lines.push(`${indent} __slotEl.innerHTML = __h;`);
479
+ lines.push(`${indent} }`);
480
+ }
481
+ }
482
+ }
483
+
292
484
  /**
293
485
  * Generate a fully self-contained JS component from a ParseResult.
294
486
  *
@@ -320,6 +512,7 @@ export function generateComponent(parseResult) {
320
512
  attrBindings = [],
321
513
  slots = [],
322
514
  constantVars = [],
515
+ watchers = [],
323
516
  refs = [],
324
517
  refBindings = [],
325
518
  childComponents = [],
@@ -329,6 +522,7 @@ export function generateComponent(parseResult) {
329
522
  const signalNames = signals.map(s => s.name);
330
523
  const computedNames = computeds.map(c => c.name);
331
524
  const constantNames = constantVars.map(v => v.name);
525
+ const methodNames = methods.map(m => m.name);
332
526
  const refVarNames = refs.map(r => r.varName);
333
527
  const propNames = new Set(propDefs.map(p => p.name));
334
528
 
@@ -454,10 +648,20 @@ export function generateComponent(parseResult) {
454
648
 
455
649
  // Computed initialization
456
650
  for (const c of computeds) {
457
- const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
651
+ const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
458
652
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
459
653
  }
460
654
 
655
+ // Watcher prev-value initialization
656
+ for (let idx = 0; idx < watchers.length; idx++) {
657
+ const w = watchers[idx];
658
+ if (w.kind === 'signal') {
659
+ lines.push(` this.__prev_${w.target} = undefined;`);
660
+ } else {
661
+ lines.push(` this.__prev_watch${idx} = undefined;`);
662
+ }
663
+ }
664
+
461
665
  // ── if: template creation, anchor reference, state init ──
462
666
  for (const ifBlock of ifBlocks) {
463
667
  const vn = ifBlock.varName;
@@ -590,14 +794,59 @@ export function generateComponent(parseResult) {
590
794
  lines.push(' });');
591
795
  }
592
796
 
797
+ // Watcher effects
798
+ for (let idx = 0; idx < watchers.length; idx++) {
799
+ const w = watchers[idx];
800
+ const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
801
+
802
+ if (w.kind === 'signal') {
803
+ // Determine the signal reference for the watch target
804
+ let watchRef;
805
+ if (propNames.has(w.target)) {
806
+ watchRef = `this._s_${w.target}()`;
807
+ } else if (computedNames.includes(w.target)) {
808
+ watchRef = `this._c_${w.target}()`;
809
+ } else {
810
+ watchRef = `this._${w.target}()`;
811
+ }
812
+ lines.push(' __effect(() => {');
813
+ lines.push(` const ${w.newParam} = ${watchRef};`);
814
+ lines.push(` if (this.__prev_${w.target} !== undefined) {`);
815
+ lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
816
+ const bodyLines = body.split('\n');
817
+ for (const line of bodyLines) {
818
+ lines.push(` ${line}`);
819
+ }
820
+ lines.push(' }');
821
+ lines.push(` this.__prev_${w.target} = ${w.newParam};`);
822
+ lines.push(' });');
823
+ } else {
824
+ // kind === 'getter' — transform the getter expression and use it directly
825
+ const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
826
+ const prevName = `__prev_watch${idx}`;
827
+ lines.push(' __effect(() => {');
828
+ lines.push(` const ${w.newParam} = ${getterExpr};`);
829
+ lines.push(` if (this.${prevName} !== undefined) {`);
830
+ lines.push(` const ${w.oldParam} = this.${prevName};`);
831
+ const bodyLines = body.split('\n');
832
+ for (const line of bodyLines) {
833
+ lines.push(` ${line}`);
834
+ }
835
+ lines.push(' }');
836
+ lines.push(` this.${prevName} = ${w.newParam};`);
837
+ lines.push(' });');
838
+ }
839
+ }
840
+
593
841
  // Event listeners
594
842
  for (const e of events) {
595
- lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
843
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
844
+ lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
596
845
  }
597
846
 
598
847
  // Show effects — one __effect per ShowBinding
599
848
  for (const sb of showBindings) {
600
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
849
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
601
850
  lines.push(' __effect(() => {');
602
851
  lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
603
852
  lines.push(' });');
@@ -639,7 +888,7 @@ export function generateComponent(parseResult) {
639
888
 
640
889
  // Attr binding effects — one __effect per AttrBinding
641
890
  for (const ab of attrBindings) {
642
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
891
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
643
892
  if (ab.kind === 'attr') {
644
893
  lines.push(' __effect(() => {');
645
894
  lines.push(` const __v = ${expr};`);
@@ -691,10 +940,10 @@ export function generateComponent(parseResult) {
691
940
  for (let i = 0; i < ifBlock.branches.length; i++) {
692
941
  const branch = ifBlock.branches[i];
693
942
  if (branch.type === 'if') {
694
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
943
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
695
944
  lines.push(` if (${expr}) { __branch = ${i}; }`);
696
945
  } else if (branch.type === 'else-if') {
697
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
946
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
698
947
  lines.push(` else if (${expr}) { __branch = ${i}; }`);
699
948
  } else {
700
949
  // else
@@ -731,7 +980,7 @@ export function generateComponent(parseResult) {
731
980
  // ── each effects ──
732
981
  for (const forBlock of forBlocks) {
733
982
  const vn = forBlock.varName;
734
- const { itemVar, indexVar, source } = forBlock;
983
+ const { itemVar, indexVar, source, keyExpr } = forBlock;
735
984
 
736
985
  const signalNamesSet = new Set(signalNames);
737
986
  const computedNamesSet = new Set(computedNames);
@@ -742,116 +991,79 @@ export function generateComponent(parseResult) {
742
991
  lines.push(' __effect(() => {');
743
992
  lines.push(` const __source = ${sourceExpr};`);
744
993
  lines.push('');
745
- lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
746
- lines.push(` this.${vn}_nodes = [];`);
747
- lines.push('');
748
994
  lines.push(" const __iter = typeof __source === 'number'");
749
995
  lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
750
996
  lines.push(' : (__source || []);');
751
997
  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
998
 
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
- }
999
+ if (keyExpr) {
1000
+ // ── Keyed reconciliation ──
1001
+ lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
1002
+ lines.push(' const __newMap = new Map();');
1003
+ lines.push(' const __newNodes = [];');
1004
+ lines.push('');
1005
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1006
+ lines.push(` const __key = ${keyExpr};`);
1007
+ lines.push(' if (__oldMap.has(__key)) {');
1008
+ lines.push(' const node = __oldMap.get(__key);');
1009
+ lines.push(' __newMap.set(__key, node);');
1010
+ lines.push(' __newNodes.push(node);');
1011
+ lines.push(' __oldMap.delete(__key);');
1012
+ lines.push(' } else {');
1013
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1014
+ lines.push(' const node = clone.firstChild;');
1015
+
1016
+ // Setup bindings/events/show/attr/model/slots for NEW nodes only
1017
+ // (reused nodes keep their existing bindings)
1018
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1019
+
1020
+ lines.push(' __newMap.set(__key, node);');
1021
+ lines.push(' __newNodes.push(node);');
1022
+ lines.push(' }');
1023
+ lines.push(' });');
1024
+ lines.push('');
1025
+ lines.push(' // Remove nodes no longer in the list');
1026
+ lines.push(' for (const n of __oldMap.values()) n.remove();');
1027
+ lines.push('');
1028
+ lines.push(' // Reorder: insert all nodes in correct order before anchor');
1029
+ lines.push(` for (const n of __newNodes) this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor);`);
1030
+ lines.push('');
1031
+ lines.push(` this.${vn}_nodes = __newNodes;`);
1032
+ lines.push(` this.${vn}_keyMap = __newMap;`);
1033
+ lines.push(' });');
1034
+ } else {
1035
+ // ── Non-keyed: destroy all and recreate (original behavior) ──
1036
+ lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
1037
+ lines.push(` this.${vn}_nodes = [];`);
1038
+ lines.push('');
1039
+ lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
1040
+ lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
1041
+ lines.push(' const node = clone.firstChild;');
803
1042
 
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
- }
1043
+ generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
826
1044
 
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
- }
1045
+ lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1046
+ lines.push(` this.${vn}_nodes.push(node);`);
1047
+ lines.push(' });');
1048
+ lines.push(' });');
841
1049
  }
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
1050
  }
848
1051
 
849
1052
  // Lifecycle: onMount hooks (at the very end of connectedCallback)
850
1053
  for (const hook of onMountHooks) {
851
1054
  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}`);
1055
+ if (hook.async) {
1056
+ lines.push(' (async () => {');
1057
+ const bodyLines = body.split('\n');
1058
+ for (const line of bodyLines) {
1059
+ lines.push(` ${line}`);
1060
+ }
1061
+ lines.push(' })();');
1062
+ } else {
1063
+ const bodyLines = body.split('\n');
1064
+ for (const line of bodyLines) {
1065
+ lines.push(` ${line}`);
1066
+ }
855
1067
  }
856
1068
  }
857
1069
 
@@ -863,9 +1075,18 @@ export function generateComponent(parseResult) {
863
1075
  lines.push(' disconnectedCallback() {');
864
1076
  for (const hook of onDestroyHooks) {
865
1077
  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}`);
1078
+ if (hook.async) {
1079
+ lines.push(' (async () => {');
1080
+ const bodyLines = body.split('\n');
1081
+ for (const line of bodyLines) {
1082
+ lines.push(` ${line}`);
1083
+ }
1084
+ lines.push(' })();');
1085
+ } else {
1086
+ const bodyLines = body.split('\n');
1087
+ for (const line of bodyLines) {
1088
+ lines.push(` ${line}`);
1089
+ }
869
1090
  }
870
1091
  }
871
1092
  lines.push(' }');
@@ -978,20 +1199,21 @@ export function generateComponent(parseResult) {
978
1199
 
979
1200
  // Events: generate addEventListener
980
1201
  for (const e of branch.events) {
1202
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
981
1203
  lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
982
- lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
1204
+ lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
983
1205
  }
984
1206
 
985
1207
  // Show bindings: generate effects
986
1208
  for (const sb of branch.showBindings) {
987
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
1209
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
988
1210
  lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
989
1211
  lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
990
1212
  }
991
1213
 
992
1214
  // Attr bindings: generate effects
993
1215
  for (const ab of branch.attrBindings) {
994
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
1216
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
995
1217
  lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
996
1218
  lines.push(` __effect(() => {`);
997
1219
  lines.push(` const __val = ${expr};`);