@sprlab/wccompiler 0.5.0 → 0.5.1

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
@@ -530,9 +530,21 @@ 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
- lines.push(`import { __signal, __computed, __effect, __batch } from '${options.runtimeImportPath}';`);
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
+ // __batch is available but only needed if user code calls it explicitly — always include for safety
545
+ usedRuntime.add('__batch');
546
+ const imports = [...usedRuntime].join(', ');
547
+ lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
536
548
  } else {
537
549
  lines.push(reactiveRuntime.trim());
538
550
  }
@@ -546,12 +558,16 @@ export function generateComponent(parseResult, options = {}) {
546
558
  lines.push('');
547
559
  }
548
560
 
549
- // ── 2. CSS injection (scoped CSS into document.head, always) ──
561
+ // ── 2. CSS injection (scoped, deduplicated via id guard) ──
550
562
  if (style) {
551
563
  const scoped = scopeCSS(style, tagName);
552
- lines.push(`const __css_${className} = document.createElement('style');`);
553
- lines.push(`__css_${className}.textContent = \`${scoped}\`;`);
554
- lines.push(`document.head.appendChild(__css_${className});`);
564
+ const cssId = `__css_${className}`;
565
+ lines.push(`if (!document.getElementById('${cssId}')) {`);
566
+ lines.push(` const ${cssId} = document.createElement('style');`);
567
+ lines.push(` ${cssId}.id = '${cssId}';`);
568
+ lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
569
+ lines.push(` document.head.appendChild(${cssId});`);
570
+ lines.push('}');
555
571
  lines.push('');
556
572
  }
557
573
 
@@ -724,8 +740,12 @@ export function generateComponent(parseResult, options = {}) {
724
740
  lines.push(' }');
725
741
  lines.push('');
726
742
 
727
- // connectedCallback
743
+ // connectedCallback (idempotent — safe for re-mount)
728
744
  lines.push(' connectedCallback() {');
745
+ lines.push(' if (this.__connected) return;');
746
+ lines.push(' this.__connected = true;');
747
+ lines.push(' this.__ac = new AbortController();');
748
+ lines.push('');
729
749
 
730
750
  // Binding effects — one __effect per binding
731
751
  for (const b of bindings) {
@@ -846,10 +866,10 @@ export function generateComponent(parseResult, options = {}) {
846
866
  }
847
867
  }
848
868
 
849
- // Event listeners
869
+ // Event listeners (with AbortController signal for cleanup)
850
870
  for (const e of events) {
851
871
  const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
852
- lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
872
+ lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
853
873
  }
854
874
 
855
875
  // Show effects — one __effect per ShowBinding
@@ -880,17 +900,17 @@ export function generateComponent(parseResult, options = {}) {
880
900
  }
881
901
  }
882
902
 
883
- // Model event listeners — DOM → signal (one addEventListener per ModelBinding)
903
+ // Model event listeners — DOM → signal (with AbortController signal)
884
904
  for (const mb of modelBindings) {
885
905
  if (mb.prop === 'checked' && mb.radioValue === null) {
886
906
  // Checkbox: read e.target.checked
887
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
907
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); }, { signal: this.__ac.signal });`);
888
908
  } else if (mb.coerce) {
889
909
  // Number input: wrap in Number()
890
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
910
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); }, { signal: this.__ac.signal });`);
891
911
  } else {
892
912
  // All others: read e.target.value
893
- lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
913
+ lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); }, { signal: this.__ac.signal });`);
894
914
  }
895
915
  }
896
916
 
@@ -1078,9 +1098,11 @@ export function generateComponent(parseResult, options = {}) {
1078
1098
  lines.push(' }');
1079
1099
  lines.push('');
1080
1100
 
1081
- // disconnectedCallback (only when destroy hooks exist)
1101
+ // disconnectedCallback (cleanup: abort listeners + user hooks)
1102
+ lines.push(' disconnectedCallback() {');
1103
+ lines.push(' this.__connected = false;');
1104
+ lines.push(' this.__ac.abort();');
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
- return generateComponent(parseResult, config);
326
+ const genOptions = { ...config, sourceFile: fileName };
327
+ return generateComponent(parseResult, genOptions);
327
328
  }
328
329
 
329
330
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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": {