@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 +13 -3
- package/lib/codegen.js +45 -18
- package/lib/compiler.js +2 -1
- package/lib/reactive-runtime.js +5 -8
- package/package.json +1 -1
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
|
-
|
|
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
|
-
// ──
|
|
533
|
-
|
|
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
|
|
561
|
+
// ── 2. CSS injection (scoped, deduplicated via id guard) ──
|
|
545
562
|
if (style) {
|
|
546
563
|
const scoped = scopeCSS(style, tagName);
|
|
547
|
-
|
|
548
|
-
lines.push(`
|
|
549
|
-
lines.push(`document.
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
326
|
+
const genOptions = { ...config, sourceFile: fileName };
|
|
327
|
+
return generateComponent(parseResult, genOptions);
|
|
327
328
|
}
|
|
328
329
|
|
|
329
330
|
/**
|
package/lib/reactive-runtime.js
CHANGED
|
@@ -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
|
|
84
|
+
const pending = [...__pendingEffects];
|
|
85
|
+
__pendingEffects.clear();
|
|
89
86
|
for (const f of pending) f();
|
|
90
87
|
}
|
|
91
88
|
}
|
package/package.json
CHANGED