@sprlab/wccompiler 0.5.0 → 0.5.2
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 +79 -57
- package/lib/compiler.js +2 -1
- package/lib/reactive-runtime.js +3 -0
- package/package.json +1 -1
package/lib/codegen.js
CHANGED
|
@@ -530,9 +530,19 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
530
530
|
|
|
531
531
|
const lines = [];
|
|
532
532
|
|
|
533
|
+
// ── 0. Source comment ──
|
|
534
|
+
if (options.sourceFile) {
|
|
535
|
+
lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
|
|
536
|
+
}
|
|
537
|
+
|
|
533
538
|
// ── 1. Reactive runtime (shared import or inline) ──
|
|
534
539
|
if (options.runtimeImportPath) {
|
|
535
|
-
|
|
540
|
+
// Tree-shake: only import what this component actually uses
|
|
541
|
+
const usedRuntime = new Set(['__signal']); // always need __signal
|
|
542
|
+
if (computeds.length > 0) usedRuntime.add('__computed');
|
|
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
|
+
const imports = [...usedRuntime].join(', ');
|
|
545
|
+
lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
|
|
536
546
|
} else {
|
|
537
547
|
lines.push(reactiveRuntime.trim());
|
|
538
548
|
}
|
|
@@ -546,12 +556,16 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
546
556
|
lines.push('');
|
|
547
557
|
}
|
|
548
558
|
|
|
549
|
-
// ── 2. CSS injection (scoped
|
|
559
|
+
// ── 2. CSS injection (scoped, deduplicated via id guard) ──
|
|
550
560
|
if (style) {
|
|
551
561
|
const scoped = scopeCSS(style, tagName);
|
|
552
|
-
|
|
553
|
-
lines.push(`
|
|
554
|
-
lines.push(`document.
|
|
562
|
+
const cssId = `__css_${className}`;
|
|
563
|
+
lines.push(`if (!document.getElementById('${cssId}')) {`);
|
|
564
|
+
lines.push(` const ${cssId} = document.createElement('style');`);
|
|
565
|
+
lines.push(` ${cssId}.id = '${cssId}';`);
|
|
566
|
+
lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
|
|
567
|
+
lines.push(` document.head.appendChild(${cssId});`);
|
|
568
|
+
lines.push('}');
|
|
555
569
|
lines.push('');
|
|
556
570
|
}
|
|
557
571
|
|
|
@@ -724,28 +738,33 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
724
738
|
lines.push(' }');
|
|
725
739
|
lines.push('');
|
|
726
740
|
|
|
727
|
-
// connectedCallback
|
|
741
|
+
// connectedCallback (idempotent — safe for re-mount)
|
|
728
742
|
lines.push(' connectedCallback() {');
|
|
743
|
+
lines.push(' if (this.__connected) return;');
|
|
744
|
+
lines.push(' this.__connected = true;');
|
|
745
|
+
lines.push(' this.__ac = new AbortController();');
|
|
746
|
+
lines.push(' this.__disposers = [];');
|
|
747
|
+
lines.push('');
|
|
729
748
|
|
|
730
749
|
// Binding effects — one __effect per binding
|
|
731
750
|
for (const b of bindings) {
|
|
732
751
|
if (b.type === 'prop') {
|
|
733
|
-
lines.push(' __effect(() => {');
|
|
752
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
734
753
|
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
735
|
-
lines.push(' });');
|
|
754
|
+
lines.push(' }));');
|
|
736
755
|
} else if (b.type === 'signal') {
|
|
737
|
-
lines.push(' __effect(() => {');
|
|
756
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
738
757
|
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
739
|
-
lines.push(' });');
|
|
758
|
+
lines.push(' }));');
|
|
740
759
|
} else if (b.type === 'computed') {
|
|
741
|
-
lines.push(' __effect(() => {');
|
|
760
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
742
761
|
lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
|
|
743
|
-
lines.push(' });');
|
|
762
|
+
lines.push(' }));');
|
|
744
763
|
} else {
|
|
745
764
|
// method type — call the method
|
|
746
|
-
lines.push(' __effect(() => {');
|
|
765
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
747
766
|
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
748
|
-
lines.push(' });');
|
|
767
|
+
lines.push(' }));');
|
|
749
768
|
}
|
|
750
769
|
}
|
|
751
770
|
|
|
@@ -784,22 +803,22 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
784
803
|
} else {
|
|
785
804
|
ref = `this._${pb.expr}()`;
|
|
786
805
|
}
|
|
787
|
-
lines.push(' __effect(() => {');
|
|
806
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
788
807
|
lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
|
|
789
|
-
lines.push(' });');
|
|
808
|
+
lines.push(' }));');
|
|
790
809
|
}
|
|
791
810
|
}
|
|
792
811
|
|
|
793
812
|
// User effects
|
|
794
813
|
for (const eff of effects) {
|
|
795
814
|
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
796
|
-
lines.push(' __effect(() => {');
|
|
815
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
797
816
|
// Indent each line of the body
|
|
798
817
|
const bodyLines = body.split('\n');
|
|
799
818
|
for (const line of bodyLines) {
|
|
800
819
|
lines.push(` ${line}`);
|
|
801
820
|
}
|
|
802
|
-
lines.push(' });');
|
|
821
|
+
lines.push(' }));');
|
|
803
822
|
}
|
|
804
823
|
|
|
805
824
|
// Watcher effects
|
|
@@ -817,7 +836,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
817
836
|
} else {
|
|
818
837
|
watchRef = `this._${w.target}()`;
|
|
819
838
|
}
|
|
820
|
-
lines.push(' __effect(() => {');
|
|
839
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
821
840
|
lines.push(` const ${w.newParam} = ${watchRef};`);
|
|
822
841
|
lines.push(` if (this.__prev_${w.target} !== undefined) {`);
|
|
823
842
|
lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
|
|
@@ -827,12 +846,12 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
827
846
|
}
|
|
828
847
|
lines.push(' }');
|
|
829
848
|
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
830
|
-
lines.push(' });');
|
|
849
|
+
lines.push(' }));');
|
|
831
850
|
} else {
|
|
832
851
|
// kind === 'getter' — transform the getter expression and use it directly
|
|
833
852
|
const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
834
853
|
const prevName = `__prev_watch${idx}`;
|
|
835
|
-
lines.push(' __effect(() => {');
|
|
854
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
836
855
|
lines.push(` const ${w.newParam} = ${getterExpr};`);
|
|
837
856
|
lines.push(` if (this.${prevName} !== undefined) {`);
|
|
838
857
|
lines.push(` const ${w.oldParam} = this.${prevName};`);
|
|
@@ -842,55 +861,55 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
842
861
|
}
|
|
843
862
|
lines.push(' }');
|
|
844
863
|
lines.push(` this.${prevName} = ${w.newParam};`);
|
|
845
|
-
lines.push(' });');
|
|
864
|
+
lines.push(' }));');
|
|
846
865
|
}
|
|
847
866
|
}
|
|
848
867
|
|
|
849
|
-
// Event listeners
|
|
868
|
+
// Event listeners (with AbortController signal for cleanup)
|
|
850
869
|
for (const e of events) {
|
|
851
870
|
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
852
|
-
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
871
|
+
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
|
|
853
872
|
}
|
|
854
873
|
|
|
855
874
|
// Show effects — one __effect per ShowBinding
|
|
856
875
|
for (const sb of showBindings) {
|
|
857
876
|
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
858
|
-
lines.push(' __effect(() => {');
|
|
877
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
859
878
|
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
860
|
-
lines.push(' });');
|
|
879
|
+
lines.push(' }));');
|
|
861
880
|
}
|
|
862
881
|
|
|
863
882
|
// Model effects — signal → DOM (one __effect per ModelBinding)
|
|
864
883
|
for (const mb of modelBindings) {
|
|
865
884
|
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
866
885
|
// Radio: compare signal value to radioValue
|
|
867
|
-
lines.push(' __effect(() => {');
|
|
886
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
868
887
|
lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
869
|
-
lines.push(' });');
|
|
888
|
+
lines.push(' }));');
|
|
870
889
|
} else if (mb.prop === 'checked') {
|
|
871
890
|
// Checkbox: coerce to boolean
|
|
872
|
-
lines.push(' __effect(() => {');
|
|
891
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
873
892
|
lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
874
|
-
lines.push(' });');
|
|
893
|
+
lines.push(' }));');
|
|
875
894
|
} else {
|
|
876
895
|
// Value-based (text, number, textarea, select): nullish coalesce to ''
|
|
877
|
-
lines.push(' __effect(() => {');
|
|
896
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
878
897
|
lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
879
|
-
lines.push(' });');
|
|
898
|
+
lines.push(' }));');
|
|
880
899
|
}
|
|
881
900
|
}
|
|
882
901
|
|
|
883
|
-
// Model event listeners — DOM → signal (
|
|
902
|
+
// Model event listeners — DOM → signal (with AbortController signal)
|
|
884
903
|
for (const mb of modelBindings) {
|
|
885
904
|
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
886
905
|
// Checkbox: read e.target.checked
|
|
887
|
-
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
906
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); }, { signal: this.__ac.signal });`);
|
|
888
907
|
} else if (mb.coerce) {
|
|
889
908
|
// Number input: wrap in Number()
|
|
890
|
-
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
909
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); }, { signal: this.__ac.signal });`);
|
|
891
910
|
} else {
|
|
892
911
|
// All others: read e.target.value
|
|
893
|
-
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
912
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); }, { signal: this.__ac.signal });`);
|
|
894
913
|
}
|
|
895
914
|
}
|
|
896
915
|
|
|
@@ -898,44 +917,44 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
898
917
|
for (const ab of attrBindings) {
|
|
899
918
|
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
900
919
|
if (ab.kind === 'attr') {
|
|
901
|
-
lines.push(' __effect(() => {');
|
|
920
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
902
921
|
lines.push(` const __v = ${expr};`);
|
|
903
922
|
lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
|
|
904
923
|
lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
905
|
-
lines.push(' });');
|
|
924
|
+
lines.push(' }));');
|
|
906
925
|
} else if (ab.kind === 'bool') {
|
|
907
|
-
lines.push(' __effect(() => {');
|
|
926
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
908
927
|
lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
|
|
909
|
-
lines.push(' });');
|
|
928
|
+
lines.push(' }));');
|
|
910
929
|
} else if (ab.kind === 'class') {
|
|
911
930
|
if (ab.expression.trimStart().startsWith('{')) {
|
|
912
931
|
// Object expression: iterate entries, classList.add/remove
|
|
913
|
-
lines.push(' __effect(() => {');
|
|
932
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
914
933
|
lines.push(` const __obj = ${expr};`);
|
|
915
934
|
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
916
935
|
lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
|
|
917
936
|
lines.push(' }');
|
|
918
|
-
lines.push(' });');
|
|
937
|
+
lines.push(' }));');
|
|
919
938
|
} else {
|
|
920
939
|
// String expression: set className
|
|
921
|
-
lines.push(' __effect(() => {');
|
|
940
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
922
941
|
lines.push(` this.${ab.varName}.className = ${expr};`);
|
|
923
|
-
lines.push(' });');
|
|
942
|
+
lines.push(' }));');
|
|
924
943
|
}
|
|
925
944
|
} else if (ab.kind === 'style') {
|
|
926
945
|
if (ab.expression.trimStart().startsWith('{')) {
|
|
927
946
|
// Object expression: iterate entries, set style[key]
|
|
928
|
-
lines.push(' __effect(() => {');
|
|
947
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
929
948
|
lines.push(` const __obj = ${expr};`);
|
|
930
949
|
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
931
950
|
lines.push(` this.${ab.varName}.style[__k] = __val;`);
|
|
932
951
|
lines.push(' }');
|
|
933
|
-
lines.push(' });');
|
|
952
|
+
lines.push(' }));');
|
|
934
953
|
} else {
|
|
935
954
|
// String expression: set cssText
|
|
936
|
-
lines.push(' __effect(() => {');
|
|
955
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
937
956
|
lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
|
|
938
|
-
lines.push(' });');
|
|
957
|
+
lines.push(' }));');
|
|
939
958
|
}
|
|
940
959
|
}
|
|
941
960
|
}
|
|
@@ -943,7 +962,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
943
962
|
// ── if effects ──
|
|
944
963
|
for (const ifBlock of ifBlocks) {
|
|
945
964
|
const vn = ifBlock.varName;
|
|
946
|
-
lines.push(' __effect(() => {');
|
|
965
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
947
966
|
lines.push(' let __branch = null;');
|
|
948
967
|
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
949
968
|
const branch = ifBlock.branches[i];
|
|
@@ -982,7 +1001,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
982
1001
|
}
|
|
983
1002
|
lines.push(' }');
|
|
984
1003
|
lines.push(` this.${vn}_active = __branch;`);
|
|
985
|
-
lines.push(' });');
|
|
1004
|
+
lines.push(' }));');
|
|
986
1005
|
}
|
|
987
1006
|
|
|
988
1007
|
// ── each effects ──
|
|
@@ -996,7 +1015,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
996
1015
|
// Transform the source expression
|
|
997
1016
|
const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
998
1017
|
|
|
999
|
-
lines.push(' __effect(() => {');
|
|
1018
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1000
1019
|
lines.push(` const __source = ${sourceExpr};`);
|
|
1001
1020
|
lines.push('');
|
|
1002
1021
|
lines.push(" const __iter = typeof __source === 'number'");
|
|
@@ -1038,7 +1057,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1038
1057
|
lines.push('');
|
|
1039
1058
|
lines.push(` this.${vn}_nodes = __newNodes;`);
|
|
1040
1059
|
lines.push(` this.${vn}_keyMap = __newMap;`);
|
|
1041
|
-
lines.push(' });');
|
|
1060
|
+
lines.push(' }));');
|
|
1042
1061
|
} else {
|
|
1043
1062
|
// ── Non-keyed: destroy all and recreate (original behavior) ──
|
|
1044
1063
|
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
@@ -1053,7 +1072,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1053
1072
|
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
1054
1073
|
lines.push(` this.${vn}_nodes.push(node);`);
|
|
1055
1074
|
lines.push(' });');
|
|
1056
|
-
lines.push(' });');
|
|
1075
|
+
lines.push(' }));');
|
|
1057
1076
|
}
|
|
1058
1077
|
}
|
|
1059
1078
|
|
|
@@ -1078,9 +1097,12 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1078
1097
|
lines.push(' }');
|
|
1079
1098
|
lines.push('');
|
|
1080
1099
|
|
|
1081
|
-
// disconnectedCallback (
|
|
1100
|
+
// disconnectedCallback (cleanup: abort listeners + dispose effects + user hooks)
|
|
1101
|
+
lines.push(' disconnectedCallback() {');
|
|
1102
|
+
lines.push(' this.__connected = false;');
|
|
1103
|
+
lines.push(' this.__ac.abort();');
|
|
1104
|
+
lines.push(' this.__disposers.forEach(d => d());');
|
|
1082
1105
|
if (onDestroyHooks.length > 0) {
|
|
1083
|
-
lines.push(' disconnectedCallback() {');
|
|
1084
1106
|
for (const hook of onDestroyHooks) {
|
|
1085
1107
|
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
1086
1108
|
if (hook.async) {
|
|
@@ -1097,9 +1119,9 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1097
1119
|
}
|
|
1098
1120
|
}
|
|
1099
1121
|
}
|
|
1100
|
-
lines.push(' }');
|
|
1101
|
-
lines.push('');
|
|
1102
1122
|
}
|
|
1123
|
+
lines.push(' }');
|
|
1124
|
+
lines.push('');
|
|
1103
1125
|
|
|
1104
1126
|
// attributeChangedCallback (if props exist)
|
|
1105
1127
|
if (propDefs.length > 0) {
|
package/lib/compiler.js
CHANGED
|
@@ -323,7 +323,8 @@ async function compileSFC(filePath, config) {
|
|
|
323
323
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
324
324
|
|
|
325
325
|
// 20. Generate component
|
|
326
|
-
|
|
326
|
+
const genOptions = { ...config, sourceFile: fileName };
|
|
327
|
+
return generateComponent(parseResult, genOptions);
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
/**
|
package/lib/reactive-runtime.js
CHANGED
|
@@ -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