@sprlab/wccompiler 0.16.2 → 0.16.4

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.
Files changed (2) hide show
  1. package/lib/codegen.js +46 -5
  2. package/package.json +1 -1
package/lib/codegen.js CHANGED
@@ -57,6 +57,47 @@ function slotPropRef(source, signalNames, computedNames, propNames) {
57
57
  return `'${source}'`;
58
58
  }
59
59
 
60
+ /**
61
+ * Wrap an expression in parentheses if it contains operators that could have
62
+ * precedence issues when combined with ?? (nullish coalescing).
63
+ *
64
+ * This prevents bugs like: this._count() || 'No items' ?? ''
65
+ * which JavaScript interprets as: this._count() || ('No items' ?? '')
66
+ *
67
+ * Operators that need wrapping:
68
+ * - Ternary: ? :
69
+ * - Logical OR: ||
70
+ * - Logical AND: &&
71
+ * - Nullish coalescing: ?? (nested)
72
+ *
73
+ * @param {string} expr - Expression to potentially wrap
74
+ * @returns {string} - Wrapped expression if it contains risky operators, otherwise unchanged
75
+ */
76
+ function wrapTernaryExpr(expr) {
77
+ const trimmed = expr.trim();
78
+
79
+ // Check for operators that have lower precedence than ?? or can cause ambiguity
80
+ // Ternary operator (? :)
81
+ const hasTernary = trimmed.includes('?') && trimmed.includes(':');
82
+
83
+ // Logical OR (||)
84
+ const hasLogicalOr = trimmed.includes('||');
85
+
86
+ // Logical AND (&&)
87
+ const hasLogicalAnd = trimmed.includes('&&');
88
+
89
+ // Nested nullish coalescing (??)
90
+ // If expression contains ??, wrap it to avoid conflict with the trailing ?? ''
91
+ const hasNullish = trimmed.includes('??');
92
+
93
+ // Wrap if any risky operator is found
94
+ if (hasTernary || hasLogicalOr || hasLogicalAnd || hasNullish) {
95
+ return `(${trimmed})`;
96
+ }
97
+
98
+ return trimmed;
99
+ }
100
+
60
101
  /**
61
102
  * Transform an expression by rewriting signal/computed variable references
62
103
  * to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
@@ -444,7 +485,7 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
444
485
  lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
445
486
  } else {
446
487
  const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
447
- lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
488
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${wrapTernaryExpr(expr)} ?? ''; });`);
448
489
  }
449
490
  }
450
491
 
@@ -650,7 +691,7 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
650
691
  lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
651
692
  } else {
652
693
  const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
653
- lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
694
+ lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${wrapTernaryExpr(expr)} ?? ''; });`);
654
695
  }
655
696
  }
656
697
 
@@ -779,7 +820,7 @@ function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, i
779
820
  lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
780
821
  } else {
781
822
  const expr = transformNested(b.name);
782
- lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
823
+ lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${wrapTernaryExpr(expr)} ?? ''; });`);
783
824
  }
784
825
  }
785
826
 
@@ -1328,7 +1369,7 @@ export function generateComponent(parseResult, options = {}) {
1328
1369
  ref = transformExpr(exprWithParens, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
1329
1370
  }
1330
1371
  lines.push(' this.__disposers.push(__effect(() => {');
1331
- lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
1372
+ lines.push(` this.${b.varName}.textContent = ${wrapTernaryExpr(ref)} ?? '';`);
1332
1373
  lines.push(' }));');
1333
1374
  }
1334
1375
  }
@@ -2016,7 +2057,7 @@ export function generateComponent(parseResult, options = {}) {
2016
2057
  } else {
2017
2058
  ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
2018
2059
  }
2019
- lines.push(` __effect(() => { ${b.varName}.textContent = ${ref} ?? ''; });`);
2060
+ lines.push(` __effect(() => { ${b.varName}.textContent = ${wrapTernaryExpr(ref)} ?? ''; });`);
2020
2061
  }
2021
2062
  }
2022
2063
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.16.2",
3
+ "version": "0.16.4",
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
  "exports": {