@sprlab/wccompiler 0.5.1 → 0.5.3

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
@@ -541,8 +541,6 @@ export function generateComponent(parseResult, options = {}) {
541
541
  const usedRuntime = new Set(['__signal']); // always need __signal
542
542
  if (computeds.length > 0) usedRuntime.add('__computed');
543
543
  if (effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0)) usedRuntime.add('__effect');
544
- // __batch is available but only needed if user code calls it explicitly — always include for safety
545
- usedRuntime.add('__batch');
546
544
  const imports = [...usedRuntime].join(', ');
547
545
  lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
548
546
  } else {
@@ -745,27 +743,35 @@ export function generateComponent(parseResult, options = {}) {
745
743
  lines.push(' if (this.__connected) return;');
746
744
  lines.push(' this.__connected = true;');
747
745
  lines.push(' this.__ac = new AbortController();');
746
+ lines.push(' this.__disposers = [];');
748
747
  lines.push('');
749
748
 
750
749
  // Binding effects — one __effect per binding
751
750
  for (const b of bindings) {
752
751
  if (b.type === 'prop') {
753
- lines.push(' __effect(() => {');
752
+ lines.push(' this.__disposers.push(__effect(() => {');
754
753
  lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
755
- lines.push(' });');
754
+ lines.push(' }));');
756
755
  } else if (b.type === 'signal') {
757
- lines.push(' __effect(() => {');
756
+ lines.push(' this.__disposers.push(__effect(() => {');
758
757
  lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
759
- lines.push(' });');
758
+ lines.push(' }));');
760
759
  } else if (b.type === 'computed') {
761
- lines.push(' __effect(() => {');
760
+ lines.push(' this.__disposers.push(__effect(() => {');
762
761
  lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
763
- lines.push(' });');
762
+ lines.push(' }));');
764
763
  } else {
765
- // method type — call the method
766
- lines.push(' __effect(() => {');
767
- lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
768
- lines.push(' });');
764
+ // method type — check if it's a props.x access pattern
765
+ let ref;
766
+ if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
767
+ const propName = b.name.slice(propsObjectName.length + 1);
768
+ ref = `this._s_${propName}()`;
769
+ } else {
770
+ ref = `this._${b.name}()`;
771
+ }
772
+ lines.push(' this.__disposers.push(__effect(() => {');
773
+ lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
774
+ lines.push(' }));');
769
775
  }
770
776
  }
771
777
 
@@ -804,22 +810,22 @@ export function generateComponent(parseResult, options = {}) {
804
810
  } else {
805
811
  ref = `this._${pb.expr}()`;
806
812
  }
807
- lines.push(' __effect(() => {');
813
+ lines.push(' this.__disposers.push(__effect(() => {');
808
814
  lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
809
- lines.push(' });');
815
+ lines.push(' }));');
810
816
  }
811
817
  }
812
818
 
813
819
  // User effects
814
820
  for (const eff of effects) {
815
821
  const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
816
- lines.push(' __effect(() => {');
822
+ lines.push(' this.__disposers.push(__effect(() => {');
817
823
  // Indent each line of the body
818
824
  const bodyLines = body.split('\n');
819
825
  for (const line of bodyLines) {
820
826
  lines.push(` ${line}`);
821
827
  }
822
- lines.push(' });');
828
+ lines.push(' }));');
823
829
  }
824
830
 
825
831
  // Watcher effects
@@ -837,7 +843,7 @@ export function generateComponent(parseResult, options = {}) {
837
843
  } else {
838
844
  watchRef = `this._${w.target}()`;
839
845
  }
840
- lines.push(' __effect(() => {');
846
+ lines.push(' this.__disposers.push(__effect(() => {');
841
847
  lines.push(` const ${w.newParam} = ${watchRef};`);
842
848
  lines.push(` if (this.__prev_${w.target} !== undefined) {`);
843
849
  lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
@@ -847,12 +853,12 @@ export function generateComponent(parseResult, options = {}) {
847
853
  }
848
854
  lines.push(' }');
849
855
  lines.push(` this.__prev_${w.target} = ${w.newParam};`);
850
- lines.push(' });');
856
+ lines.push(' }));');
851
857
  } else {
852
858
  // kind === 'getter' — transform the getter expression and use it directly
853
859
  const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
854
860
  const prevName = `__prev_watch${idx}`;
855
- lines.push(' __effect(() => {');
861
+ lines.push(' this.__disposers.push(__effect(() => {');
856
862
  lines.push(` const ${w.newParam} = ${getterExpr};`);
857
863
  lines.push(` if (this.${prevName} !== undefined) {`);
858
864
  lines.push(` const ${w.oldParam} = this.${prevName};`);
@@ -862,7 +868,7 @@ export function generateComponent(parseResult, options = {}) {
862
868
  }
863
869
  lines.push(' }');
864
870
  lines.push(` this.${prevName} = ${w.newParam};`);
865
- lines.push(' });');
871
+ lines.push(' }));');
866
872
  }
867
873
  }
868
874
 
@@ -875,28 +881,28 @@ export function generateComponent(parseResult, options = {}) {
875
881
  // Show effects — one __effect per ShowBinding
876
882
  for (const sb of showBindings) {
877
883
  const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
878
- lines.push(' __effect(() => {');
884
+ lines.push(' this.__disposers.push(__effect(() => {');
879
885
  lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
880
- lines.push(' });');
886
+ lines.push(' }));');
881
887
  }
882
888
 
883
889
  // Model effects — signal → DOM (one __effect per ModelBinding)
884
890
  for (const mb of modelBindings) {
885
891
  if (mb.prop === 'checked' && mb.radioValue !== null) {
886
892
  // Radio: compare signal value to radioValue
887
- lines.push(' __effect(() => {');
893
+ lines.push(' this.__disposers.push(__effect(() => {');
888
894
  lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
889
- lines.push(' });');
895
+ lines.push(' }));');
890
896
  } else if (mb.prop === 'checked') {
891
897
  // Checkbox: coerce to boolean
892
- lines.push(' __effect(() => {');
898
+ lines.push(' this.__disposers.push(__effect(() => {');
893
899
  lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
894
- lines.push(' });');
900
+ lines.push(' }));');
895
901
  } else {
896
902
  // Value-based (text, number, textarea, select): nullish coalesce to ''
897
- lines.push(' __effect(() => {');
903
+ lines.push(' this.__disposers.push(__effect(() => {');
898
904
  lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
899
- lines.push(' });');
905
+ lines.push(' }));');
900
906
  }
901
907
  }
902
908
 
@@ -918,44 +924,44 @@ export function generateComponent(parseResult, options = {}) {
918
924
  for (const ab of attrBindings) {
919
925
  const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
920
926
  if (ab.kind === 'attr') {
921
- lines.push(' __effect(() => {');
927
+ lines.push(' this.__disposers.push(__effect(() => {');
922
928
  lines.push(` const __v = ${expr};`);
923
929
  lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
924
930
  lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
925
- lines.push(' });');
931
+ lines.push(' }));');
926
932
  } else if (ab.kind === 'bool') {
927
- lines.push(' __effect(() => {');
933
+ lines.push(' this.__disposers.push(__effect(() => {');
928
934
  lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
929
- lines.push(' });');
935
+ lines.push(' }));');
930
936
  } else if (ab.kind === 'class') {
931
937
  if (ab.expression.trimStart().startsWith('{')) {
932
938
  // Object expression: iterate entries, classList.add/remove
933
- lines.push(' __effect(() => {');
939
+ lines.push(' this.__disposers.push(__effect(() => {');
934
940
  lines.push(` const __obj = ${expr};`);
935
941
  lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
936
942
  lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
937
943
  lines.push(' }');
938
- lines.push(' });');
944
+ lines.push(' }));');
939
945
  } else {
940
946
  // String expression: set className
941
- lines.push(' __effect(() => {');
947
+ lines.push(' this.__disposers.push(__effect(() => {');
942
948
  lines.push(` this.${ab.varName}.className = ${expr};`);
943
- lines.push(' });');
949
+ lines.push(' }));');
944
950
  }
945
951
  } else if (ab.kind === 'style') {
946
952
  if (ab.expression.trimStart().startsWith('{')) {
947
953
  // Object expression: iterate entries, set style[key]
948
- lines.push(' __effect(() => {');
954
+ lines.push(' this.__disposers.push(__effect(() => {');
949
955
  lines.push(` const __obj = ${expr};`);
950
956
  lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
951
957
  lines.push(` this.${ab.varName}.style[__k] = __val;`);
952
958
  lines.push(' }');
953
- lines.push(' });');
959
+ lines.push(' }));');
954
960
  } else {
955
961
  // String expression: set cssText
956
- lines.push(' __effect(() => {');
962
+ lines.push(' this.__disposers.push(__effect(() => {');
957
963
  lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
958
- lines.push(' });');
964
+ lines.push(' }));');
959
965
  }
960
966
  }
961
967
  }
@@ -963,7 +969,7 @@ export function generateComponent(parseResult, options = {}) {
963
969
  // ── if effects ──
964
970
  for (const ifBlock of ifBlocks) {
965
971
  const vn = ifBlock.varName;
966
- lines.push(' __effect(() => {');
972
+ lines.push(' this.__disposers.push(__effect(() => {');
967
973
  lines.push(' let __branch = null;');
968
974
  for (let i = 0; i < ifBlock.branches.length; i++) {
969
975
  const branch = ifBlock.branches[i];
@@ -1002,7 +1008,7 @@ export function generateComponent(parseResult, options = {}) {
1002
1008
  }
1003
1009
  lines.push(' }');
1004
1010
  lines.push(` this.${vn}_active = __branch;`);
1005
- lines.push(' });');
1011
+ lines.push(' }));');
1006
1012
  }
1007
1013
 
1008
1014
  // ── each effects ──
@@ -1016,7 +1022,7 @@ export function generateComponent(parseResult, options = {}) {
1016
1022
  // Transform the source expression
1017
1023
  const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
1018
1024
 
1019
- lines.push(' __effect(() => {');
1025
+ lines.push(' this.__disposers.push(__effect(() => {');
1020
1026
  lines.push(` const __source = ${sourceExpr};`);
1021
1027
  lines.push('');
1022
1028
  lines.push(" const __iter = typeof __source === 'number'");
@@ -1058,7 +1064,7 @@ export function generateComponent(parseResult, options = {}) {
1058
1064
  lines.push('');
1059
1065
  lines.push(` this.${vn}_nodes = __newNodes;`);
1060
1066
  lines.push(` this.${vn}_keyMap = __newMap;`);
1061
- lines.push(' });');
1067
+ lines.push(' }));');
1062
1068
  } else {
1063
1069
  // ── Non-keyed: destroy all and recreate (original behavior) ──
1064
1070
  lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
@@ -1073,7 +1079,7 @@ export function generateComponent(parseResult, options = {}) {
1073
1079
  lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
1074
1080
  lines.push(` this.${vn}_nodes.push(node);`);
1075
1081
  lines.push(' });');
1076
- lines.push(' });');
1082
+ lines.push(' }));');
1077
1083
  }
1078
1084
  }
1079
1085
 
@@ -1098,10 +1104,11 @@ export function generateComponent(parseResult, options = {}) {
1098
1104
  lines.push(' }');
1099
1105
  lines.push('');
1100
1106
 
1101
- // disconnectedCallback (cleanup: abort listeners + user hooks)
1107
+ // disconnectedCallback (cleanup: abort listeners + dispose effects + user hooks)
1102
1108
  lines.push(' disconnectedCallback() {');
1103
1109
  lines.push(' this.__connected = false;');
1104
1110
  lines.push(' this.__ac.abort();');
1111
+ lines.push(' this.__disposers.forEach(d => d());');
1105
1112
  if (onDestroyHooks.length > 0) {
1106
1113
  for (const hook of onDestroyHooks) {
1107
1114
  const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
@@ -64,7 +64,9 @@ function __computed(fn) {
64
64
 
65
65
  function __effect(fn) {
66
66
  let _cleanup = null;
67
+ let _active = true;
67
68
  const run = () => {
69
+ if (!_active) return;
68
70
  if (typeof _cleanup === 'function') _cleanup();
69
71
  const prev = __currentEffect;
70
72
  __currentEffect = run;
@@ -72,6 +74,7 @@ function __effect(fn) {
72
74
  __currentEffect = prev;
73
75
  };
74
76
  run();
77
+ return () => { _active = false; if (typeof _cleanup === 'function') _cleanup(); };
75
78
  }
76
79
 
77
80
  function __batch(fn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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": {