@sprlab/wccompiler 0.3.0 → 0.4.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
@@ -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
@@ -430,11 +517,13 @@ export function generateComponent(parseResult) {
430
517
  refBindings = [],
431
518
  childComponents = [],
432
519
  childImports = [],
520
+ exposeNames = [],
433
521
  } = parseResult;
434
522
 
435
523
  const signalNames = signals.map(s => s.name);
436
524
  const computedNames = computeds.map(c => c.name);
437
525
  const constantNames = constantVars.map(v => v.name);
526
+ const methodNames = methods.map(m => m.name);
438
527
  const refVarNames = refs.map(r => r.varName);
439
528
  const propNames = new Set(propDefs.map(p => p.name));
440
529
 
@@ -526,9 +615,11 @@ export function generateComponent(parseResult) {
526
615
  lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
527
616
  }
528
617
 
529
- // Assign DOM refs for child component instances
618
+ // Assign DOM refs for child component instances (only if they have prop bindings)
530
619
  for (const cc of childComponents) {
531
- lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
620
+ if (cc.propBindings.length > 0) {
621
+ lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
622
+ }
532
623
  }
533
624
 
534
625
  // Assign DOM refs for attr bindings (reuse ref when same path)
@@ -560,13 +651,18 @@ export function generateComponent(parseResult) {
560
651
 
561
652
  // Computed initialization
562
653
  for (const c of computeds) {
563
- const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
654
+ const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
564
655
  lines.push(` this._c_${c.name} = __computed(() => ${body});`);
565
656
  }
566
657
 
567
658
  // Watcher prev-value initialization
568
- for (const w of watchers) {
569
- lines.push(` this.__prev_${w.target} = undefined;`);
659
+ for (let idx = 0; idx < watchers.length; idx++) {
660
+ const w = watchers[idx];
661
+ if (w.kind === 'signal') {
662
+ lines.push(` this.__prev_${w.target} = undefined;`);
663
+ } else {
664
+ lines.push(` this.__prev_watch${idx} = undefined;`);
665
+ }
570
666
  }
571
667
 
572
668
  // ── if: template creation, anchor reference, state init ──
@@ -702,38 +798,58 @@ export function generateComponent(parseResult) {
702
798
  }
703
799
 
704
800
  // 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
- }
801
+ for (let idx = 0; idx < watchers.length; idx++) {
802
+ const w = watchers[idx];
715
803
  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}`);
804
+
805
+ if (w.kind === 'signal') {
806
+ // Determine the signal reference for the watch target
807
+ let watchRef;
808
+ if (propNames.has(w.target)) {
809
+ watchRef = `this._s_${w.target}()`;
810
+ } else if (computedNames.includes(w.target)) {
811
+ watchRef = `this._c_${w.target}()`;
812
+ } else {
813
+ watchRef = `this._${w.target}()`;
814
+ }
815
+ lines.push(' __effect(() => {');
816
+ lines.push(` const ${w.newParam} = ${watchRef};`);
817
+ lines.push(` if (this.__prev_${w.target} !== undefined) {`);
818
+ lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
819
+ const bodyLines = body.split('\n');
820
+ for (const line of bodyLines) {
821
+ lines.push(` ${line}`);
822
+ }
823
+ lines.push(' }');
824
+ lines.push(` this.__prev_${w.target} = ${w.newParam};`);
825
+ lines.push(' });');
826
+ } else {
827
+ // kind === 'getter' — transform the getter expression and use it directly
828
+ const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
829
+ const prevName = `__prev_watch${idx}`;
830
+ lines.push(' __effect(() => {');
831
+ lines.push(` const ${w.newParam} = ${getterExpr};`);
832
+ lines.push(` if (this.${prevName} !== undefined) {`);
833
+ lines.push(` const ${w.oldParam} = this.${prevName};`);
834
+ const bodyLines = body.split('\n');
835
+ for (const line of bodyLines) {
836
+ lines.push(` ${line}`);
837
+ }
838
+ lines.push(' }');
839
+ lines.push(` this.${prevName} = ${w.newParam};`);
840
+ lines.push(' });');
723
841
  }
724
- lines.push(' }');
725
- lines.push(` this.__prev_${w.target} = ${w.newParam};`);
726
- lines.push(' });');
727
842
  }
728
843
 
729
844
  // Event listeners
730
845
  for (const e of events) {
731
- lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
846
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
847
+ lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
732
848
  }
733
849
 
734
850
  // Show effects — one __effect per ShowBinding
735
851
  for (const sb of showBindings) {
736
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
852
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
737
853
  lines.push(' __effect(() => {');
738
854
  lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
739
855
  lines.push(' });');
@@ -775,7 +891,7 @@ export function generateComponent(parseResult) {
775
891
 
776
892
  // Attr binding effects — one __effect per AttrBinding
777
893
  for (const ab of attrBindings) {
778
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
894
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
779
895
  if (ab.kind === 'attr') {
780
896
  lines.push(' __effect(() => {');
781
897
  lines.push(` const __v = ${expr};`);
@@ -827,10 +943,10 @@ export function generateComponent(parseResult) {
827
943
  for (let i = 0; i < ifBlock.branches.length; i++) {
828
944
  const branch = ifBlock.branches[i];
829
945
  if (branch.type === 'if') {
830
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
946
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
831
947
  lines.push(` if (${expr}) { __branch = ${i}; }`);
832
948
  } else if (branch.type === 'else-if') {
833
- const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
949
+ const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
834
950
  lines.push(` else if (${expr}) { __branch = ${i}; }`);
835
951
  } else {
836
952
  // else
@@ -1044,6 +1160,20 @@ export function generateComponent(parseResult) {
1044
1160
  }
1045
1161
  }
1046
1162
 
1163
+ // ── defineExpose: public getters/methods ──
1164
+ for (const name of exposeNames) {
1165
+ if (computedNames.includes(name)) {
1166
+ lines.push(` get ${name}() { return this._c_${name}(); }`);
1167
+ } else if (signalNames.includes(name)) {
1168
+ lines.push(` get ${name}() { return this._${name}(); }`);
1169
+ } else if (methodNames.includes(name)) {
1170
+ lines.push(` ${name}(...args) { return this._${name}(...args); }`);
1171
+ } else if (constantNames.includes(name)) {
1172
+ lines.push(` get ${name}() { return this._const_${name}; }`);
1173
+ }
1174
+ }
1175
+ if (exposeNames.length > 0) lines.push('');
1176
+
1047
1177
  // ── if setup methods ──
1048
1178
  for (const ifBlock of ifBlocks) {
1049
1179
  const vn = ifBlock.varName;
@@ -1086,20 +1216,21 @@ export function generateComponent(parseResult) {
1086
1216
 
1087
1217
  // Events: generate addEventListener
1088
1218
  for (const e of branch.events) {
1219
+ const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
1089
1220
  lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
1090
- lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
1221
+ lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
1091
1222
  }
1092
1223
 
1093
1224
  // Show bindings: generate effects
1094
1225
  for (const sb of branch.showBindings) {
1095
- const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
1226
+ const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
1096
1227
  lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
1097
1228
  lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
1098
1229
  }
1099
1230
 
1100
1231
  // Attr bindings: generate effects
1101
1232
  for (const ab of branch.attrBindings) {
1102
- const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
1233
+ const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
1103
1234
  lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
1104
1235
  lines.push(` __effect(() => {`);
1105
1236
  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
+