@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/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
- r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
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
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
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
- r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
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
- lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
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 (const w of watchers) {
569
- lines.push(` this.__prev_${w.target} = undefined;`);
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 (const w of watchers) {
706
- // Determine the signal reference for the watch target
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
- lines.push(' __effect(() => {');
717
- lines.push(` const ${w.newParam} = ${watchRef};`);
718
- lines.push(` if (this.__prev_${w.target} !== undefined) {`);
719
- lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
720
- const bodyLines = body.split('\n');
721
- for (const line of bodyLines) {
722
- lines.push(` ${line}`);
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
- lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
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}', this._${e.handler}.bind(this));`);
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};`);
@@ -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
+