@sprlab/wccompiler 0.5.12 → 0.5.13

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
@@ -479,6 +479,325 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
479
479
  lines.push(`${indent} }`);
480
480
  }
481
481
  }
482
+
483
+ // Nested each directives (forBlocks)
484
+ for (const innerFor of (forBlock.forBlocks || [])) {
485
+ const innerVn = innerFor.varName;
486
+ const innerItemVar = innerFor.itemVar;
487
+ const innerIndexVar = innerFor.indexVar;
488
+ const innerSource = innerFor.source;
489
+ const innerKeyExpr = innerFor.keyExpr;
490
+
491
+ // Build excludeSet that includes BOTH outer and inner loop variables
492
+ // so transformForExpr does not rewrite them as signals
493
+ const outerExcludeVars = [itemVar];
494
+ if (indexVar) outerExcludeVars.push(indexVar);
495
+ const innerExcludeVars = [innerItemVar];
496
+ if (innerIndexVar) innerExcludeVars.push(innerIndexVar);
497
+
498
+ // Create inner template element
499
+ lines.push(`${indent} const ${innerVn}_tpl = document.createElement('template');`);
500
+ lines.push(`${indent} ${innerVn}_tpl.innerHTML = \`${innerFor.templateHtml}\`;`);
501
+
502
+ // Find inner anchor comment in the cloned outer item node
503
+ lines.push(`${indent} const ${innerVn}_anchor = ${pathExpr(innerFor.anchorPath, 'node')};`);
504
+
505
+ // Transform the inner source expression (may reference outer item var)
506
+ const innerSourceExpr = transformForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
507
+
508
+ // Determine if inner source is static (only references outer loop vars)
509
+ const innerSourceIsStatic = isStaticForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
510
+
511
+ if (innerKeyExpr) {
512
+ // ── Keyed reconciliation for nested each ──
513
+ lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
514
+ lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
515
+ lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
516
+ lines.push(`${indent} : (${innerVn}_source || []);`);
517
+ lines.push(`${indent} const ${innerVn}_newNodes = [];`);
518
+ lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
519
+ lines.push(`${indent} const __key = ${innerKeyExpr};`);
520
+ lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
521
+ lines.push(`${indent} const innerNode = clone.firstChild;`);
522
+
523
+ // Generate inner item bindings with combined excludeSet
524
+ generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
525
+
526
+ lines.push(`${indent} ${innerVn}_newNodes.push(innerNode);`);
527
+ lines.push(`${indent} });`);
528
+ lines.push(`${indent} for (const n of ${innerVn}_newNodes) { ${innerVn}_anchor.parentNode.insertBefore(n, ${innerVn}_anchor); }`);
529
+ } else {
530
+ // ── Non-keyed nested each: iterate and clone ──
531
+ lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
532
+ lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
533
+ lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
534
+ lines.push(`${indent} : (${innerVn}_source || []);`);
535
+ lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
536
+ lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
537
+ lines.push(`${indent} const innerNode = clone.firstChild;`);
538
+
539
+ // Generate inner item bindings with combined excludeSet
540
+ generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
541
+
542
+ lines.push(`${indent} ${innerVn}_anchor.parentNode.insertBefore(innerNode, ${innerVn}_anchor);`);
543
+ lines.push(`${indent} });`);
544
+ }
545
+ }
546
+
547
+ // Nested if/else-if/else chains (ifBlocks)
548
+ for (const ifBlock of (forBlock.ifBlocks || [])) {
549
+ const vn = ifBlock.varName;
550
+ const branches = ifBlock.branches;
551
+
552
+ // 3.1: Create template elements for each branch
553
+ for (let i = 0; i < branches.length; i++) {
554
+ const branch = branches[i];
555
+ lines.push(`${indent} const ${vn}_t${i} = document.createElement('template');`);
556
+ lines.push(`${indent} ${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
557
+ }
558
+
559
+ // 3.1: Find anchor comment in the cloned node
560
+ lines.push(`${indent} const ${vn}_anchor = ${pathExpr(ifBlock.anchorPath, 'node')};`);
561
+
562
+ // 3.2: Generate per-item conditional evaluation (static, not reactive)
563
+ lines.push(`${indent} let ${vn}_branch = null;`);
564
+ for (let i = 0; i < branches.length; i++) {
565
+ const branch = branches[i];
566
+ if (branch.type === 'if') {
567
+ lines.push(`${indent} if (${branch.expression}) { ${vn}_branch = ${i}; }`);
568
+ } else if (branch.type === 'else-if') {
569
+ lines.push(`${indent} else if (${branch.expression}) { ${vn}_branch = ${i}; }`);
570
+ } else {
571
+ // else
572
+ lines.push(`${indent} else { ${vn}_branch = ${i}; }`);
573
+ }
574
+ }
575
+
576
+ // 3.3: Insert only the matching branch node and apply branch bindings/events/show/attr/model
577
+ lines.push(`${indent} if (${vn}_branch !== null) {`);
578
+ const tplArray = branches.map((_, i) => `${vn}_t${i}`).join(', ');
579
+ lines.push(`${indent} const ${vn}_tpl = [${tplArray}][${vn}_branch];`);
580
+ lines.push(`${indent} const ${vn}_clone = ${vn}_tpl.content.cloneNode(true);`);
581
+ lines.push(`${indent} const ${vn}_node = ${vn}_clone.firstChild;`);
582
+ lines.push(`${indent} ${vn}_anchor.parentNode.insertBefore(${vn}_node, ${vn}_anchor);`);
583
+
584
+ // Apply branch bindings/events/show/attr/model using the outer loop's item variable
585
+ const hasSetup = branches.some(b =>
586
+ (b.bindings && b.bindings.length > 0) ||
587
+ (b.events && b.events.length > 0) ||
588
+ (b.showBindings && b.showBindings.length > 0) ||
589
+ (b.attrBindings && b.attrBindings.length > 0) ||
590
+ (b.modelBindings && b.modelBindings.length > 0)
591
+ );
592
+ if (hasSetup) {
593
+ // Generate per-branch setup inline (static evaluation using item variable)
594
+ for (let i = 0; i < branches.length; i++) {
595
+ const branch = branches[i];
596
+ const hasBranchSetup =
597
+ (branch.bindings && branch.bindings.length > 0) ||
598
+ (branch.events && branch.events.length > 0) ||
599
+ (branch.showBindings && branch.showBindings.length > 0) ||
600
+ (branch.attrBindings && branch.attrBindings.length > 0) ||
601
+ (branch.modelBindings && branch.modelBindings.length > 0);
602
+ if (!hasBranchSetup) continue;
603
+
604
+ const keyword = i === 0 ? 'if' : 'else if';
605
+ lines.push(`${indent} ${keyword} (${vn}_branch === ${i}) {`);
606
+
607
+ // Bindings (static: use item var directly)
608
+ for (const b of branch.bindings) {
609
+ const nodeRef = pathExpr(b.path, `${vn}_node`);
610
+ if (isStaticForBinding(b.name, itemVar, indexVar)) {
611
+ lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
612
+ } else {
613
+ const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
614
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
615
+ }
616
+ }
617
+
618
+ // Events
619
+ for (const e of branch.events) {
620
+ const nodeRef = pathExpr(e.path, `${vn}_node`);
621
+ const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
622
+ lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
623
+ }
624
+
625
+ // Show bindings
626
+ for (const sb of (branch.showBindings || [])) {
627
+ const nodeRef = pathExpr(sb.path, `${vn}_node`);
628
+ if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
629
+ lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
630
+ } else {
631
+ const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
632
+ lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
633
+ }
634
+ }
635
+
636
+ // Attr bindings
637
+ for (const ab of (branch.attrBindings || [])) {
638
+ const nodeRef = pathExpr(ab.path, `${vn}_node`);
639
+ if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
640
+ lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
641
+ lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
642
+ } else {
643
+ const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
644
+ lines.push(`${indent} __effect(() => {`);
645
+ lines.push(`${indent} const __val = ${expr};`);
646
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
647
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
648
+ lines.push(`${indent} });`);
649
+ }
650
+ }
651
+
652
+ // Model bindings
653
+ for (const mb of (branch.modelBindings || [])) {
654
+ const nodeRef = pathExpr(mb.path, `${vn}_node`);
655
+ lines.push(`${indent} __effect(() => {`);
656
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
657
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
658
+ } else if (mb.prop === 'checked') {
659
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
660
+ } else {
661
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
662
+ }
663
+ lines.push(`${indent} });`);
664
+ if (mb.prop === 'checked' && mb.radioValue === null) {
665
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
666
+ } else if (mb.coerce) {
667
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
668
+ } else {
669
+ lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
670
+ }
671
+ }
672
+
673
+ lines.push(`${indent} }`);
674
+ }
675
+ }
676
+ lines.push(`${indent} }`);
677
+ }
678
+ }
679
+
680
+ /**
681
+ * Generate inner item bindings/events/show/attr/model for a nested each directive.
682
+ * Uses transformForExpr with an excludeSet that includes BOTH outer and inner loop variables.
683
+ *
684
+ * @param {string[]} lines - Output lines array
685
+ * @param {ForBlock} innerFor - The nested ForBlock
686
+ * @param {string} outerItemVar - Outer loop item variable
687
+ * @param {string|null} outerIndexVar - Outer loop index variable
688
+ * @param {string} innerItemVar - Inner loop item variable
689
+ * @param {string|null} innerIndexVar - Inner loop index variable
690
+ * @param {Set<string>} propNames - Prop names set
691
+ * @param {Set<string>} signalNamesSet - Signal names set
692
+ * @param {Set<string>} computedNamesSet - Computed names set
693
+ * @param {string} indent - Current indentation
694
+ */
695
+ function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent) {
696
+ // Build combined exclude set with both outer and inner loop variables
697
+ const combinedExcludeItemVar = innerItemVar;
698
+ const combinedExcludeIndexVar = innerIndexVar;
699
+
700
+ // For transformForExpr, we need to ensure both outer and inner vars are excluded.
701
+ // We create a modified propNames/signalNamesSet/computedNamesSet that doesn't include
702
+ // any of the loop variables. transformForExpr already excludes itemVar/indexVar,
703
+ // but we also need to exclude the outer loop variables.
704
+ // Strategy: filter out outer loop vars from the sets passed to transformForExpr
705
+ const filteredSignalNames = new Set([...signalNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
706
+ const filteredComputedNames = new Set([...computedNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
707
+ const filteredPropNames = new Set([...propNames].filter(n => n !== outerItemVar && n !== outerIndexVar));
708
+
709
+ // Helper: check if expression is static (only references inner/outer loop vars, no signals/computeds/props)
710
+ function isNestedStatic(expr) {
711
+ // An expression is static if it only references the loop variables (outer + inner)
712
+ const allExclude = new Set([innerItemVar, outerItemVar]);
713
+ if (innerIndexVar) allExclude.add(innerIndexVar);
714
+ if (outerIndexVar) allExclude.add(outerIndexVar);
715
+
716
+ for (const p of propNames) {
717
+ if (allExclude.has(p)) continue;
718
+ if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
719
+ }
720
+ for (const n of signalNamesSet) {
721
+ if (allExclude.has(n)) continue;
722
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
723
+ }
724
+ for (const n of computedNamesSet) {
725
+ if (allExclude.has(n)) continue;
726
+ if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
727
+ }
728
+ return true;
729
+ }
730
+
731
+ // Helper: transform expression excluding both outer and inner loop vars
732
+ function transformNested(expr) {
733
+ return transformForExpr(expr, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
734
+ }
735
+
736
+ // Bindings
737
+ for (const b of innerFor.bindings) {
738
+ const nodeRef = pathExpr(b.path, 'innerNode');
739
+ if (isNestedStatic(b.name)) {
740
+ lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
741
+ } else {
742
+ const expr = transformNested(b.name);
743
+ lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
744
+ }
745
+ }
746
+
747
+ // Events
748
+ for (const e of innerFor.events) {
749
+ const nodeRef = pathExpr(e.path, 'innerNode');
750
+ const handlerExpr = generateForEventHandler(e.handler, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
751
+ lines.push(`${indent}${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
752
+ }
753
+
754
+ // Show
755
+ for (const sb of innerFor.showBindings) {
756
+ const nodeRef = pathExpr(sb.path, 'innerNode');
757
+ if (isNestedStatic(sb.expression)) {
758
+ lines.push(`${indent}${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
759
+ } else {
760
+ const expr = transformNested(sb.expression);
761
+ lines.push(`${indent}__effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
762
+ }
763
+ }
764
+
765
+ // Attr bindings
766
+ for (const ab of innerFor.attrBindings) {
767
+ const nodeRef = pathExpr(ab.path, 'innerNode');
768
+ if (isNestedStatic(ab.expression)) {
769
+ lines.push(`${indent}const __val_${ab.varName} = ${ab.expression};`);
770
+ lines.push(`${indent}if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
771
+ } else {
772
+ const expr = transformNested(ab.expression);
773
+ lines.push(`${indent}__effect(() => {`);
774
+ lines.push(`${indent} const __val = ${expr};`);
775
+ lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
776
+ lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
777
+ lines.push(`${indent}});`);
778
+ }
779
+ }
780
+
781
+ // Model bindings
782
+ for (const mb of (innerFor.modelBindings || [])) {
783
+ const nodeRef = pathExpr(mb.path, 'innerNode');
784
+ lines.push(`${indent}__effect(() => {`);
785
+ if (mb.prop === 'checked' && mb.radioValue !== null) {
786
+ lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
787
+ } else if (mb.prop === 'checked') {
788
+ lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
789
+ } else {
790
+ lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
791
+ }
792
+ lines.push(`${indent}});`);
793
+ if (mb.prop === 'checked' && mb.radioValue === null) {
794
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
795
+ } else if (mb.coerce) {
796
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
797
+ } else {
798
+ lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
799
+ }
800
+ }
482
801
  }
483
802
 
484
803
  /**
@@ -372,7 +372,14 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
372
372
  // Use walkTree on the branch root to discover bindings/events
373
373
  const result = walkTree(branchRoot, signalNames, computedNames, propNames);
374
374
 
375
- // Capture the processed HTML AFTER walkTree has modified the DOM
375
+ // Detect nested each directives within the branch template
376
+ const forBlocks = processForBlocks(branchRoot, [], signalNames, computedNames, propNames);
377
+
378
+ // Detect nested if/else-if/else chains within the branch template
379
+ const ifBlocks = processIfChains(branchRoot, [], signalNames, computedNames, propNames);
380
+
381
+ // Capture the processed HTML AFTER all processing (walkTree + processForBlocks + processIfChains)
382
+ // since processForBlocks/processIfChains modify the DOM by replacing elements with comment nodes
376
383
  const processedHtml = branchRoot.innerHTML;
377
384
 
378
385
  // Strip the first path segment from all paths since at runtime
@@ -392,6 +399,17 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
392
399
  stripFirstSegment(result.slots);
393
400
  stripFirstSegment(result.childComponents);
394
401
 
402
+ // Strip first path segment from nested forBlock/ifBlock anchor paths
403
+ function stripFirstAnchorSegment(items) {
404
+ for (const item of items) {
405
+ if (item.anchorPath && item.anchorPath.length > 0 && item.anchorPath[0].startsWith('childNodes[')) {
406
+ item.anchorPath = item.anchorPath.slice(1);
407
+ }
408
+ }
409
+ }
410
+ stripFirstAnchorSegment(forBlocks);
411
+ stripFirstAnchorSegment(ifBlocks);
412
+
395
413
  return {
396
414
  bindings: result.bindings,
397
415
  events: result.events,
@@ -400,6 +418,8 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
400
418
  modelBindings: result.modelBindings,
401
419
  slots: result.slots,
402
420
  childComponents: result.childComponents,
421
+ forBlocks,
422
+ ifBlocks,
403
423
  processedHtml,
404
424
  };
405
425
  }
@@ -763,7 +783,7 @@ export function processForBlocks(parent, parentPath, signalNames, computedNames,
763
783
  const templateHtml = clone.outerHTML;
764
784
 
765
785
  // Process internal bindings/events via partial walk
766
- const { bindings, events, showBindings, attrBindings, modelBindings, slots, childComponents: forChildComponents, processedHtml } = walkBranch(templateHtml, signalNames, computedNames, propNames);
786
+ const { bindings, events, showBindings, attrBindings, modelBindings, slots, childComponents: forChildComponents, forBlocks: nestedForBlocks, ifBlocks: nestedIfBlocks, processedHtml } = walkBranch(templateHtml, signalNames, computedNames, propNames);
767
787
 
768
788
  // Replace the original element with a comment node <!-- each -->
769
789
  const doc = node.ownerDocument;
@@ -792,6 +812,8 @@ export function processForBlocks(parent, parentPath, signalNames, computedNames,
792
812
  modelBindings,
793
813
  slots,
794
814
  childComponents: forChildComponents,
815
+ forBlocks: nestedForBlocks,
816
+ ifBlocks: nestedIfBlocks,
795
817
  });
796
818
  } else {
797
819
  // Recurse into non-each elements to find nested each
package/lib/types.js CHANGED
@@ -153,6 +153,8 @@
153
153
  * @property {AttrBinding[]} attrBindings — :attr bindings within item
154
154
  * @property {ModelBinding[]} modelBindings — model bindings within item
155
155
  * @property {SlotBinding[]} slots — slot bindings within item
156
+ * @property {ForBlock[]} [forBlocks] — Nested each directives within item
157
+ * @property {IfBlock[]} [ifBlocks] — Nested if/else-if/else chains within item
156
158
  */
157
159
 
158
160
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "bin": {