@sprlab/wccompiler 0.4.3 → 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/bin/wcc.js CHANGED
@@ -15,16 +15,28 @@ async function build(config, cwd) {
15
15
 
16
16
  if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
17
17
 
18
+ // Generate shared reactive runtime
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+ const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
22
+ const signalsContent = reactiveRuntime.trim().replace(/^/gm, '') + '\nexport { __signal, __computed, __effect, __batch };\n';
23
+ const signalsDest = join(outputDir, '__wcc-signals.js');
24
+ writeFileSync(signalsDest, signalsContent);
25
+
18
26
  // Discover source files
19
27
  const files = discoverFiles(inputDir);
20
28
  let errors = 0;
21
29
 
22
30
  for (const file of files) {
23
31
  try {
24
- const output = await compile(file);
32
+ // Calculate relative path from component output to __wcc-signals.js
25
33
  const relPath = relative(inputDir, file);
26
34
  const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
27
35
  const outDir = dirname(outPath);
36
+ const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
37
+ const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
38
+
39
+ const output = await compile(file, { runtimeImportPath });
28
40
  if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
29
41
  writeFileSync(outPath, output);
30
42
  } catch (err) {
@@ -34,8 +46,6 @@ async function build(config, cwd) {
34
46
  }
35
47
 
36
48
  // Copy wcc-runtime.js to output directory
37
- const __filename = fileURLToPath(import.meta.url);
38
- const __dirname = dirname(__filename);
39
49
  const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
40
50
  const runtimeDest = join(outputDir, 'wcc-runtime.js');
41
51
  copyFileSync(runtimeSrc, runtimeDest);
package/lib/codegen.js CHANGED
@@ -485,9 +485,10 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
485
485
  * Generate a fully self-contained JS component from a ParseResult.
486
486
  *
487
487
  * @param {ParseResult} parseResult — Complete IR with bindings/events
488
+ * @param {{ runtimeImportPath?: string }} [options] — Optional generation options
488
489
  * @returns {string} JavaScript source code
489
490
  */
490
- export function generateComponent(parseResult) {
491
+ export function generateComponent(parseResult, options = {}) {
491
492
  const {
492
493
  tagName,
493
494
  className,
@@ -529,8 +530,24 @@ export function generateComponent(parseResult) {
529
530
 
530
531
  const lines = [];
531
532
 
532
- // ── 1. Inline reactive runtime ──
533
- lines.push(reactiveRuntime.trim());
533
+ // ── 0. Source comment ──
534
+ if (options.sourceFile) {
535
+ lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
536
+ }
537
+
538
+ // ── 1. Reactive runtime (shared import or inline) ──
539
+ if (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}';`);
548
+ } else {
549
+ lines.push(reactiveRuntime.trim());
550
+ }
534
551
  lines.push('');
535
552
 
536
553
  // ── 1b. Child component imports ──
@@ -541,12 +558,16 @@ export function generateComponent(parseResult) {
541
558
  lines.push('');
542
559
  }
543
560
 
544
- // ── 2. CSS injection (scoped CSS into document.head, always) ──
561
+ // ── 2. CSS injection (scoped, deduplicated via id guard) ──
545
562
  if (style) {
546
563
  const scoped = scopeCSS(style, tagName);
547
- lines.push(`const __css_${className} = document.createElement('style');`);
548
- lines.push(`__css_${className}.textContent = \`${scoped}\`;`);
549
- 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('}');
550
571
  lines.push('');
551
572
  }
552
573
 
@@ -719,8 +740,12 @@ export function generateComponent(parseResult) {
719
740
  lines.push(' }');
720
741
  lines.push('');
721
742
 
722
- // connectedCallback
743
+ // connectedCallback (idempotent — safe for re-mount)
723
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('');
724
749
 
725
750
  // Binding effects — one __effect per binding
726
751
  for (const b of bindings) {
@@ -841,10 +866,10 @@ export function generateComponent(parseResult) {
841
866
  }
842
867
  }
843
868
 
844
- // Event listeners
869
+ // Event listeners (with AbortController signal for cleanup)
845
870
  for (const e of events) {
846
871
  const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
847
- lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
872
+ lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
848
873
  }
849
874
 
850
875
  // Show effects — one __effect per ShowBinding
@@ -875,17 +900,17 @@ export function generateComponent(parseResult) {
875
900
  }
876
901
  }
877
902
 
878
- // Model event listeners — DOM → signal (one addEventListener per ModelBinding)
903
+ // Model event listeners — DOM → signal (with AbortController signal)
879
904
  for (const mb of modelBindings) {
880
905
  if (mb.prop === 'checked' && mb.radioValue === null) {
881
906
  // Checkbox: read e.target.checked
882
- 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 });`);
883
908
  } else if (mb.coerce) {
884
909
  // Number input: wrap in Number()
885
- 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 });`);
886
911
  } else {
887
912
  // All others: read e.target.value
888
- 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 });`);
889
914
  }
890
915
  }
891
916
 
@@ -1073,9 +1098,11 @@ export function generateComponent(parseResult) {
1073
1098
  lines.push(' }');
1074
1099
  lines.push('');
1075
1100
 
1076
- // 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();');
1077
1105
  if (onDestroyHooks.length > 0) {
1078
- lines.push(' disconnectedCallback() {');
1079
1106
  for (const hook of onDestroyHooks) {
1080
1107
  const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
1081
1108
  if (hook.async) {
@@ -1092,9 +1119,9 @@ export function generateComponent(parseResult) {
1092
1119
  }
1093
1120
  }
1094
1121
  }
1095
- lines.push(' }');
1096
- lines.push('');
1097
1122
  }
1123
+ lines.push(' }');
1124
+ lines.push('');
1098
1125
 
1099
1126
  // attributeChangedCallback (if props exist)
1100
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);
326
+ const genOptions = { ...config, sourceFile: fileName };
327
+ return generateComponent(parseResult, genOptions);
327
328
  }
328
329
 
329
330
  /**
@@ -16,7 +16,7 @@
16
16
  export const reactiveRuntime = `
17
17
  let __currentEffect = null;
18
18
  let __batchDepth = 0;
19
- const __pendingEffects = [];
19
+ const __pendingEffects = new Set();
20
20
 
21
21
  function __signal(initial) {
22
22
  let _value = initial;
@@ -30,9 +30,7 @@ function __signal(initial) {
30
30
  _value = args[0];
31
31
  if (old !== _value) {
32
32
  if (__batchDepth > 0) {
33
- for (const fn of _subs) {
34
- if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
35
- }
33
+ for (const fn of _subs) __pendingEffects.add(fn);
36
34
  } else {
37
35
  for (const fn of [..._subs]) fn();
38
36
  }
@@ -46,9 +44,7 @@ function __computed(fn) {
46
44
  const recompute = () => {
47
45
  _dirty = true;
48
46
  if (__batchDepth > 0) {
49
- for (const fn of _subs) {
50
- if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
51
- }
47
+ for (const fn of _subs) __pendingEffects.add(fn);
52
48
  } else {
53
49
  for (const fn of [..._subs]) fn();
54
50
  }
@@ -85,7 +81,8 @@ function __batch(fn) {
85
81
  } finally {
86
82
  __batchDepth--;
87
83
  if (__batchDepth === 0) {
88
- const pending = __pendingEffects.splice(0);
84
+ const pending = [...__pendingEffects];
85
+ __pendingEffects.clear();
89
86
  for (const f of pending) f();
90
87
  }
91
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.4.3",
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": {