@sprlab/wccompiler 0.3.0 → 0.4.0
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/README.md +130 -37
- package/bin/wcc.js +4 -5
- package/bin/wcc.test.js +23 -16
- package/lib/codegen.js +150 -36
- package/lib/compiler-browser.js +21 -0
- package/lib/compiler.js +225 -91
- package/lib/parser-extractors.js +20 -21
- package/lib/sfc-parser.js +262 -0
- package/lib/tree-walker.js +18 -10
- package/lib/types.js +2 -1
- package/package.json +3 -3
- package/types/wcc.d.ts +4 -5
- package/types/wcc.test.js +2 -2
- package/lib/printer.js +0 -118
package/lib/codegen.js
CHANGED
|
@@ -75,7 +75,7 @@ function slotPropRef(source, signalNames, computedNames, propNames) {
|
|
|
75
75
|
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
76
76
|
* @returns {string}
|
|
77
77
|
*/
|
|
78
|
-
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = []) {
|
|
78
|
+
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = []) {
|
|
79
79
|
let result = expr;
|
|
80
80
|
|
|
81
81
|
// Transform emit calls: emitsObjectName( → this._emit(
|
|
@@ -84,6 +84,14 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
|
|
|
84
84
|
result = result.replace(emitsRe, 'this._emit(');
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Transform method calls: methodName( → this._methodName(
|
|
88
|
+
for (const name of methodNames) {
|
|
89
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
90
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
91
|
+
const methodRe = new RegExp(`\\b${name}\\(`, 'g');
|
|
92
|
+
result = result.replace(methodRe, `this._${name}(`);
|
|
93
|
+
}
|
|
94
|
+
|
|
87
95
|
// Transform props.x → this._s_x() BEFORE signal/computed transforms
|
|
88
96
|
if (propsObjectName && propNames.size > 0) {
|
|
89
97
|
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
@@ -239,15 +247,24 @@ export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames
|
|
|
239
247
|
|
|
240
248
|
for (const p of propsSet) {
|
|
241
249
|
if (excludeSet.has(p)) continue;
|
|
242
|
-
|
|
250
|
+
// First: transform name() calls → this._s_name() (don't double-call)
|
|
251
|
+
r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
|
|
252
|
+
// Then: transform bare name references
|
|
253
|
+
r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
|
|
243
254
|
}
|
|
244
255
|
for (const n of rootVarNames) {
|
|
245
256
|
if (excludeSet.has(n)) continue;
|
|
246
|
-
|
|
257
|
+
// First: transform name() calls → this._name() (don't double-call)
|
|
258
|
+
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
|
|
259
|
+
// Then: transform bare name references
|
|
260
|
+
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
|
|
247
261
|
}
|
|
248
262
|
for (const n of computedNames) {
|
|
249
263
|
if (excludeSet.has(n)) continue;
|
|
250
|
-
|
|
264
|
+
// First: transform name() calls → this._c_name() (don't double-call)
|
|
265
|
+
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
|
|
266
|
+
// Then: transform bare name references
|
|
267
|
+
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
|
|
251
268
|
}
|
|
252
269
|
return r;
|
|
253
270
|
}
|
|
@@ -297,6 +314,75 @@ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames,
|
|
|
297
314
|
return true;
|
|
298
315
|
}
|
|
299
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Generate the JS expression for an event handler based on its type:
|
|
319
|
+
* - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
|
|
320
|
+
* - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
|
|
321
|
+
* - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
|
|
322
|
+
*
|
|
323
|
+
* @param {string} handler — The raw handler string from the template
|
|
324
|
+
* @param {string[]} signalNames
|
|
325
|
+
* @param {string[]} computedNames
|
|
326
|
+
* @param {string|null} propsObjectName
|
|
327
|
+
* @param {Set<string>} propNames
|
|
328
|
+
* @param {string|null} emitsObjectName
|
|
329
|
+
* @param {string[]} constantNames
|
|
330
|
+
* @returns {string}
|
|
331
|
+
*/
|
|
332
|
+
export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames) {
|
|
333
|
+
if (handler.includes('=>')) {
|
|
334
|
+
// Arrow function expression: (e) => removeItem(item)
|
|
335
|
+
const arrowIdx = handler.indexOf('=>');
|
|
336
|
+
const params = handler.slice(0, arrowIdx).trim();
|
|
337
|
+
let body = handler.slice(arrowIdx + 2).trim();
|
|
338
|
+
body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames);
|
|
339
|
+
return `${params} => { ${body}; }`;
|
|
340
|
+
} else if (handler.includes('(')) {
|
|
341
|
+
// Function call expression: removeItem(item)
|
|
342
|
+
const parenIdx = handler.indexOf('(');
|
|
343
|
+
const fnName = handler.slice(0, parenIdx).trim();
|
|
344
|
+
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
345
|
+
const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames) : '';
|
|
346
|
+
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
347
|
+
} else {
|
|
348
|
+
// Simple method name
|
|
349
|
+
return `this._${handler}.bind(this)`;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Generate the JS expression for an event handler inside an each block.
|
|
355
|
+
* Similar to generateEventHandler but uses transformForExpr for the each scope.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} handler
|
|
358
|
+
* @param {string} itemVar
|
|
359
|
+
* @param {string|null} indexVar
|
|
360
|
+
* @param {Set<string>} propNames
|
|
361
|
+
* @param {Set<string>} signalNamesSet
|
|
362
|
+
* @param {Set<string>} computedNamesSet
|
|
363
|
+
* @returns {string}
|
|
364
|
+
*/
|
|
365
|
+
export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
366
|
+
if (handler.includes('=>')) {
|
|
367
|
+
// Arrow function expression
|
|
368
|
+
const arrowIdx = handler.indexOf('=>');
|
|
369
|
+
const params = handler.slice(0, arrowIdx).trim();
|
|
370
|
+
let body = handler.slice(arrowIdx + 2).trim();
|
|
371
|
+
body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
372
|
+
return `${params} => { ${body}; }`;
|
|
373
|
+
} else if (handler.includes('(')) {
|
|
374
|
+
// Function call expression: removeItem(item)
|
|
375
|
+
const parenIdx = handler.indexOf('(');
|
|
376
|
+
const fnName = handler.slice(0, parenIdx).trim();
|
|
377
|
+
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
378
|
+
const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
|
|
379
|
+
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
380
|
+
} else {
|
|
381
|
+
// Simple method name
|
|
382
|
+
return `this._${handler}.bind(this)`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
300
386
|
/**
|
|
301
387
|
* Generate per-item setup code for bindings, events, show, attr, model, and slots.
|
|
302
388
|
* Used by both keyed and non-keyed each effects.
|
|
@@ -326,7 +412,8 @@ function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signal
|
|
|
326
412
|
// Events
|
|
327
413
|
for (const e of forBlock.events) {
|
|
328
414
|
const nodeRef = pathExpr(e.path, 'node');
|
|
329
|
-
|
|
415
|
+
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
416
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
330
417
|
}
|
|
331
418
|
|
|
332
419
|
// Show
|
|
@@ -435,6 +522,7 @@ export function generateComponent(parseResult) {
|
|
|
435
522
|
const signalNames = signals.map(s => s.name);
|
|
436
523
|
const computedNames = computeds.map(c => c.name);
|
|
437
524
|
const constantNames = constantVars.map(v => v.name);
|
|
525
|
+
const methodNames = methods.map(m => m.name);
|
|
438
526
|
const refVarNames = refs.map(r => r.varName);
|
|
439
527
|
const propNames = new Set(propDefs.map(p => p.name));
|
|
440
528
|
|
|
@@ -560,13 +648,18 @@ export function generateComponent(parseResult) {
|
|
|
560
648
|
|
|
561
649
|
// Computed initialization
|
|
562
650
|
for (const c of computeds) {
|
|
563
|
-
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
651
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
564
652
|
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
565
653
|
}
|
|
566
654
|
|
|
567
655
|
// Watcher prev-value initialization
|
|
568
|
-
for (
|
|
569
|
-
|
|
656
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
657
|
+
const w = watchers[idx];
|
|
658
|
+
if (w.kind === 'signal') {
|
|
659
|
+
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
660
|
+
} else {
|
|
661
|
+
lines.push(` this.__prev_watch${idx} = undefined;`);
|
|
662
|
+
}
|
|
570
663
|
}
|
|
571
664
|
|
|
572
665
|
// ── if: template creation, anchor reference, state init ──
|
|
@@ -702,38 +795,58 @@ export function generateComponent(parseResult) {
|
|
|
702
795
|
}
|
|
703
796
|
|
|
704
797
|
// Watcher effects
|
|
705
|
-
for (
|
|
706
|
-
|
|
707
|
-
let watchRef;
|
|
708
|
-
if (propNames.has(w.target)) {
|
|
709
|
-
watchRef = `this._s_${w.target}()`;
|
|
710
|
-
} else if (computedNames.includes(w.target)) {
|
|
711
|
-
watchRef = `this._c_${w.target}()`;
|
|
712
|
-
} else {
|
|
713
|
-
watchRef = `this._${w.target}()`;
|
|
714
|
-
}
|
|
798
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
799
|
+
const w = watchers[idx];
|
|
715
800
|
const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
801
|
+
|
|
802
|
+
if (w.kind === 'signal') {
|
|
803
|
+
// Determine the signal reference for the watch target
|
|
804
|
+
let watchRef;
|
|
805
|
+
if (propNames.has(w.target)) {
|
|
806
|
+
watchRef = `this._s_${w.target}()`;
|
|
807
|
+
} else if (computedNames.includes(w.target)) {
|
|
808
|
+
watchRef = `this._c_${w.target}()`;
|
|
809
|
+
} else {
|
|
810
|
+
watchRef = `this._${w.target}()`;
|
|
811
|
+
}
|
|
812
|
+
lines.push(' __effect(() => {');
|
|
813
|
+
lines.push(` const ${w.newParam} = ${watchRef};`);
|
|
814
|
+
lines.push(` if (this.__prev_${w.target} !== undefined) {`);
|
|
815
|
+
lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
|
|
816
|
+
const bodyLines = body.split('\n');
|
|
817
|
+
for (const line of bodyLines) {
|
|
818
|
+
lines.push(` ${line}`);
|
|
819
|
+
}
|
|
820
|
+
lines.push(' }');
|
|
821
|
+
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
822
|
+
lines.push(' });');
|
|
823
|
+
} else {
|
|
824
|
+
// kind === 'getter' — transform the getter expression and use it directly
|
|
825
|
+
const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
826
|
+
const prevName = `__prev_watch${idx}`;
|
|
827
|
+
lines.push(' __effect(() => {');
|
|
828
|
+
lines.push(` const ${w.newParam} = ${getterExpr};`);
|
|
829
|
+
lines.push(` if (this.${prevName} !== undefined) {`);
|
|
830
|
+
lines.push(` const ${w.oldParam} = this.${prevName};`);
|
|
831
|
+
const bodyLines = body.split('\n');
|
|
832
|
+
for (const line of bodyLines) {
|
|
833
|
+
lines.push(` ${line}`);
|
|
834
|
+
}
|
|
835
|
+
lines.push(' }');
|
|
836
|
+
lines.push(` this.${prevName} = ${w.newParam};`);
|
|
837
|
+
lines.push(' });');
|
|
723
838
|
}
|
|
724
|
-
lines.push(' }');
|
|
725
|
-
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
726
|
-
lines.push(' });');
|
|
727
839
|
}
|
|
728
840
|
|
|
729
841
|
// Event listeners
|
|
730
842
|
for (const e of events) {
|
|
731
|
-
|
|
843
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
844
|
+
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
732
845
|
}
|
|
733
846
|
|
|
734
847
|
// Show effects — one __effect per ShowBinding
|
|
735
848
|
for (const sb of showBindings) {
|
|
736
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
849
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
737
850
|
lines.push(' __effect(() => {');
|
|
738
851
|
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
739
852
|
lines.push(' });');
|
|
@@ -775,7 +888,7 @@ export function generateComponent(parseResult) {
|
|
|
775
888
|
|
|
776
889
|
// Attr binding effects — one __effect per AttrBinding
|
|
777
890
|
for (const ab of attrBindings) {
|
|
778
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
891
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
779
892
|
if (ab.kind === 'attr') {
|
|
780
893
|
lines.push(' __effect(() => {');
|
|
781
894
|
lines.push(` const __v = ${expr};`);
|
|
@@ -827,10 +940,10 @@ export function generateComponent(parseResult) {
|
|
|
827
940
|
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
828
941
|
const branch = ifBlock.branches[i];
|
|
829
942
|
if (branch.type === 'if') {
|
|
830
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
943
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
831
944
|
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
832
945
|
} else if (branch.type === 'else-if') {
|
|
833
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
946
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
834
947
|
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
835
948
|
} else {
|
|
836
949
|
// else
|
|
@@ -1086,20 +1199,21 @@ export function generateComponent(parseResult) {
|
|
|
1086
1199
|
|
|
1087
1200
|
// Events: generate addEventListener
|
|
1088
1201
|
for (const e of branch.events) {
|
|
1202
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1089
1203
|
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
1090
|
-
lines.push(` ${e.varName}.addEventListener('${e.event}',
|
|
1204
|
+
lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
1091
1205
|
}
|
|
1092
1206
|
|
|
1093
1207
|
// Show bindings: generate effects
|
|
1094
1208
|
for (const sb of branch.showBindings) {
|
|
1095
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1209
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1096
1210
|
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
1097
1211
|
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
1098
1212
|
}
|
|
1099
1213
|
|
|
1100
1214
|
// Attr bindings: generate effects
|
|
1101
1215
|
for (const ab of branch.attrBindings) {
|
|
1102
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1216
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
1103
1217
|
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
1104
1218
|
lines.push(` __effect(() => {`);
|
|
1105
1219
|
lines.push(` const __val = ${expr};`);
|
package/lib/compiler-browser.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
|
|
44
44
|
import { generateComponent } from './codegen.js';
|
|
45
45
|
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
46
|
+
import { parseSFC } from './sfc-parser.js';
|
|
46
47
|
|
|
47
48
|
// ── Browser-compatible DOM helpers ──────────────────────────────────
|
|
48
49
|
|
|
@@ -503,3 +504,23 @@ export async function compileFromStrings({ script, template, style = '', tag, la
|
|
|
503
504
|
});
|
|
504
505
|
}
|
|
505
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Compile an SFC component from a source string (browser-compatible).
|
|
509
|
+
* Parses the SFC to extract blocks, then delegates to compileFromStrings.
|
|
510
|
+
*
|
|
511
|
+
* @param {string} source — Full content of the .wcc file
|
|
512
|
+
* @param {{ stripTypes?: (code: string) => Promise<string> }} [options]
|
|
513
|
+
* @returns {Promise<string>} Compiled JavaScript
|
|
514
|
+
*/
|
|
515
|
+
export async function compileFromSFC(source, options) {
|
|
516
|
+
const descriptor = parseSFC(source);
|
|
517
|
+
return compileFromStrings({
|
|
518
|
+
script: descriptor.script,
|
|
519
|
+
template: descriptor.template,
|
|
520
|
+
style: descriptor.style,
|
|
521
|
+
tag: descriptor.tag,
|
|
522
|
+
lang: descriptor.lang,
|
|
523
|
+
stripTypes: options?.stripTypes,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|