@sprlab/wccompiler 0.2.1 → 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 +154 -34
- package/bin/wcc.js +4 -5
- package/bin/wcc.test.js +23 -16
- package/lib/codegen.js +336 -114
- package/lib/compiler-browser.js +526 -0
- package/lib/compiler.js +225 -91
- package/lib/dev-server.js +55 -17
- package/lib/parser-extractors.js +1029 -0
- package/lib/parser.js +36 -929
- package/lib/reactive-runtime.js +35 -4
- package/lib/sfc-parser.js +262 -0
- package/lib/tree-walker.js +18 -10
- package/lib/types.js +11 -0
- package/package.json +3 -3
- package/types/wcc.d.ts +6 -6
- 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');
|
|
@@ -95,6 +103,14 @@ export function transformExpr(expr, signalNames, computedNames, propsObjectName
|
|
|
95
103
|
});
|
|
96
104
|
}
|
|
97
105
|
|
|
106
|
+
// Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
|
|
107
|
+
for (const propName of propNames) {
|
|
108
|
+
if (propsObjectName && propName === propsObjectName) continue;
|
|
109
|
+
if (emitsObjectName && propName === emitsObjectName) continue;
|
|
110
|
+
const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
111
|
+
result = result.replace(bareRe, `this._s_${propName}()`);
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
// Transform computed names first (to avoid partial matches with signals)
|
|
99
115
|
for (const name of computedNames) {
|
|
100
116
|
// Skip propsObjectName and emitsObjectName
|
|
@@ -231,15 +247,24 @@ export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames
|
|
|
231
247
|
|
|
232
248
|
for (const p of propsSet) {
|
|
233
249
|
if (excludeSet.has(p)) continue;
|
|
234
|
-
|
|
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}()`);
|
|
235
254
|
}
|
|
236
255
|
for (const n of rootVarNames) {
|
|
237
256
|
if (excludeSet.has(n)) continue;
|
|
238
|
-
|
|
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}()`);
|
|
239
261
|
}
|
|
240
262
|
for (const n of computedNames) {
|
|
241
263
|
if (excludeSet.has(n)) continue;
|
|
242
|
-
|
|
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}()`);
|
|
243
268
|
}
|
|
244
269
|
return r;
|
|
245
270
|
}
|
|
@@ -289,6 +314,173 @@ export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames,
|
|
|
289
314
|
return true;
|
|
290
315
|
}
|
|
291
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
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Generate per-item setup code for bindings, events, show, attr, model, and slots.
|
|
388
|
+
* Used by both keyed and non-keyed each effects.
|
|
389
|
+
*
|
|
390
|
+
* @param {string[]} lines — Output lines array
|
|
391
|
+
* @param {object} forBlock — ForBlock with bindings, events, etc.
|
|
392
|
+
* @param {string} itemVar
|
|
393
|
+
* @param {string|null} indexVar
|
|
394
|
+
* @param {Set<string>} propNames
|
|
395
|
+
* @param {Set<string>} signalNamesSet
|
|
396
|
+
* @param {Set<string>} computedNamesSet
|
|
397
|
+
*/
|
|
398
|
+
function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
399
|
+
const indent = ' ';
|
|
400
|
+
|
|
401
|
+
// Bindings
|
|
402
|
+
for (const b of forBlock.bindings) {
|
|
403
|
+
const nodeRef = pathExpr(b.path, 'node');
|
|
404
|
+
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
405
|
+
lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
406
|
+
} else {
|
|
407
|
+
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
408
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Events
|
|
413
|
+
for (const e of forBlock.events) {
|
|
414
|
+
const nodeRef = pathExpr(e.path, 'node');
|
|
415
|
+
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
416
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Show
|
|
420
|
+
for (const sb of forBlock.showBindings) {
|
|
421
|
+
const nodeRef = pathExpr(sb.path, 'node');
|
|
422
|
+
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
423
|
+
lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
424
|
+
} else {
|
|
425
|
+
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
426
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Attr bindings
|
|
431
|
+
for (const ab of forBlock.attrBindings) {
|
|
432
|
+
const nodeRef = pathExpr(ab.path, 'node');
|
|
433
|
+
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
434
|
+
lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
|
|
435
|
+
lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
436
|
+
} else {
|
|
437
|
+
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
438
|
+
lines.push(`${indent} __effect(() => {`);
|
|
439
|
+
lines.push(`${indent} const __val = ${expr};`);
|
|
440
|
+
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
441
|
+
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
442
|
+
lines.push(`${indent} });`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Model bindings
|
|
447
|
+
for (const mb of (forBlock.modelBindings || [])) {
|
|
448
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
449
|
+
lines.push(`${indent} __effect(() => {`);
|
|
450
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
451
|
+
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
452
|
+
} else if (mb.prop === 'checked') {
|
|
453
|
+
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
454
|
+
} else {
|
|
455
|
+
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
456
|
+
}
|
|
457
|
+
lines.push(`${indent} });`);
|
|
458
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
459
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
460
|
+
} else if (mb.coerce) {
|
|
461
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
462
|
+
} else {
|
|
463
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Scoped slots
|
|
468
|
+
for (const s of (forBlock.slots || [])) {
|
|
469
|
+
if (s.slotProps.length > 0) {
|
|
470
|
+
const slotNodeRef = pathExpr(s.path, 'node');
|
|
471
|
+
const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
|
|
472
|
+
lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
|
|
473
|
+
lines.push(`${indent} const __sp = { ${propsEntries} };`);
|
|
474
|
+
lines.push(`${indent} let __h = __slotEl.innerHTML;`);
|
|
475
|
+
lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
|
|
476
|
+
lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
477
|
+
lines.push(`${indent} }`);
|
|
478
|
+
lines.push(`${indent} __slotEl.innerHTML = __h;`);
|
|
479
|
+
lines.push(`${indent} }`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
292
484
|
/**
|
|
293
485
|
* Generate a fully self-contained JS component from a ParseResult.
|
|
294
486
|
*
|
|
@@ -320,6 +512,7 @@ export function generateComponent(parseResult) {
|
|
|
320
512
|
attrBindings = [],
|
|
321
513
|
slots = [],
|
|
322
514
|
constantVars = [],
|
|
515
|
+
watchers = [],
|
|
323
516
|
refs = [],
|
|
324
517
|
refBindings = [],
|
|
325
518
|
childComponents = [],
|
|
@@ -329,6 +522,7 @@ export function generateComponent(parseResult) {
|
|
|
329
522
|
const signalNames = signals.map(s => s.name);
|
|
330
523
|
const computedNames = computeds.map(c => c.name);
|
|
331
524
|
const constantNames = constantVars.map(v => v.name);
|
|
525
|
+
const methodNames = methods.map(m => m.name);
|
|
332
526
|
const refVarNames = refs.map(r => r.varName);
|
|
333
527
|
const propNames = new Set(propDefs.map(p => p.name));
|
|
334
528
|
|
|
@@ -454,10 +648,20 @@ export function generateComponent(parseResult) {
|
|
|
454
648
|
|
|
455
649
|
// Computed initialization
|
|
456
650
|
for (const c of computeds) {
|
|
457
|
-
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
651
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
458
652
|
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
459
653
|
}
|
|
460
654
|
|
|
655
|
+
// Watcher prev-value initialization
|
|
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
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
461
665
|
// ── if: template creation, anchor reference, state init ──
|
|
462
666
|
for (const ifBlock of ifBlocks) {
|
|
463
667
|
const vn = ifBlock.varName;
|
|
@@ -590,14 +794,59 @@ export function generateComponent(parseResult) {
|
|
|
590
794
|
lines.push(' });');
|
|
591
795
|
}
|
|
592
796
|
|
|
797
|
+
// Watcher effects
|
|
798
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
799
|
+
const w = watchers[idx];
|
|
800
|
+
const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
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(' });');
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
593
841
|
// Event listeners
|
|
594
842
|
for (const e of events) {
|
|
595
|
-
|
|
843
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
844
|
+
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
596
845
|
}
|
|
597
846
|
|
|
598
847
|
// Show effects — one __effect per ShowBinding
|
|
599
848
|
for (const sb of showBindings) {
|
|
600
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
849
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
601
850
|
lines.push(' __effect(() => {');
|
|
602
851
|
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
603
852
|
lines.push(' });');
|
|
@@ -639,7 +888,7 @@ export function generateComponent(parseResult) {
|
|
|
639
888
|
|
|
640
889
|
// Attr binding effects — one __effect per AttrBinding
|
|
641
890
|
for (const ab of attrBindings) {
|
|
642
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
891
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
643
892
|
if (ab.kind === 'attr') {
|
|
644
893
|
lines.push(' __effect(() => {');
|
|
645
894
|
lines.push(` const __v = ${expr};`);
|
|
@@ -691,10 +940,10 @@ export function generateComponent(parseResult) {
|
|
|
691
940
|
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
692
941
|
const branch = ifBlock.branches[i];
|
|
693
942
|
if (branch.type === 'if') {
|
|
694
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
943
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
695
944
|
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
696
945
|
} else if (branch.type === 'else-if') {
|
|
697
|
-
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
946
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
698
947
|
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
699
948
|
} else {
|
|
700
949
|
// else
|
|
@@ -731,7 +980,7 @@ export function generateComponent(parseResult) {
|
|
|
731
980
|
// ── each effects ──
|
|
732
981
|
for (const forBlock of forBlocks) {
|
|
733
982
|
const vn = forBlock.varName;
|
|
734
|
-
const { itemVar, indexVar, source } = forBlock;
|
|
983
|
+
const { itemVar, indexVar, source, keyExpr } = forBlock;
|
|
735
984
|
|
|
736
985
|
const signalNamesSet = new Set(signalNames);
|
|
737
986
|
const computedNamesSet = new Set(computedNames);
|
|
@@ -742,116 +991,79 @@ export function generateComponent(parseResult) {
|
|
|
742
991
|
lines.push(' __effect(() => {');
|
|
743
992
|
lines.push(` const __source = ${sourceExpr};`);
|
|
744
993
|
lines.push('');
|
|
745
|
-
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
746
|
-
lines.push(` this.${vn}_nodes = [];`);
|
|
747
|
-
lines.push('');
|
|
748
994
|
lines.push(" const __iter = typeof __source === 'number'");
|
|
749
995
|
lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
|
|
750
996
|
lines.push(' : (__source || []);');
|
|
751
997
|
lines.push('');
|
|
752
|
-
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
753
|
-
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
754
|
-
lines.push(' const node = clone.firstChild;');
|
|
755
|
-
|
|
756
|
-
// Setup bindings per item
|
|
757
|
-
for (const b of forBlock.bindings) {
|
|
758
|
-
const nodeRef = pathExpr(b.path, 'node');
|
|
759
|
-
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
760
|
-
// Static binding: reference only item/index, assign once
|
|
761
|
-
lines.push(` ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
762
|
-
} else {
|
|
763
|
-
// Reactive binding: references component variables, wrap in effect
|
|
764
|
-
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
765
|
-
lines.push(` __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
998
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
lines.push(
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
}
|
|
802
|
-
|
|
999
|
+
if (keyExpr) {
|
|
1000
|
+
// ── Keyed reconciliation ──
|
|
1001
|
+
lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
|
|
1002
|
+
lines.push(' const __newMap = new Map();');
|
|
1003
|
+
lines.push(' const __newNodes = [];');
|
|
1004
|
+
lines.push('');
|
|
1005
|
+
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
1006
|
+
lines.push(` const __key = ${keyExpr};`);
|
|
1007
|
+
lines.push(' if (__oldMap.has(__key)) {');
|
|
1008
|
+
lines.push(' const node = __oldMap.get(__key);');
|
|
1009
|
+
lines.push(' __newMap.set(__key, node);');
|
|
1010
|
+
lines.push(' __newNodes.push(node);');
|
|
1011
|
+
lines.push(' __oldMap.delete(__key);');
|
|
1012
|
+
lines.push(' } else {');
|
|
1013
|
+
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
1014
|
+
lines.push(' const node = clone.firstChild;');
|
|
1015
|
+
|
|
1016
|
+
// Setup bindings/events/show/attr/model/slots for NEW nodes only
|
|
1017
|
+
// (reused nodes keep their existing bindings)
|
|
1018
|
+
generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
1019
|
+
|
|
1020
|
+
lines.push(' __newMap.set(__key, node);');
|
|
1021
|
+
lines.push(' __newNodes.push(node);');
|
|
1022
|
+
lines.push(' }');
|
|
1023
|
+
lines.push(' });');
|
|
1024
|
+
lines.push('');
|
|
1025
|
+
lines.push(' // Remove nodes no longer in the list');
|
|
1026
|
+
lines.push(' for (const n of __oldMap.values()) n.remove();');
|
|
1027
|
+
lines.push('');
|
|
1028
|
+
lines.push(' // Reorder: insert all nodes in correct order before anchor');
|
|
1029
|
+
lines.push(` for (const n of __newNodes) this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor);`);
|
|
1030
|
+
lines.push('');
|
|
1031
|
+
lines.push(` this.${vn}_nodes = __newNodes;`);
|
|
1032
|
+
lines.push(` this.${vn}_keyMap = __newMap;`);
|
|
1033
|
+
lines.push(' });');
|
|
1034
|
+
} else {
|
|
1035
|
+
// ── Non-keyed: destroy all and recreate (original behavior) ──
|
|
1036
|
+
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
1037
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
1038
|
+
lines.push('');
|
|
1039
|
+
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
1040
|
+
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
1041
|
+
lines.push(' const node = clone.firstChild;');
|
|
803
1042
|
|
|
804
|
-
|
|
805
|
-
for (const mb of (forBlock.modelBindings || [])) {
|
|
806
|
-
const nodeRef = pathExpr(mb.path, 'node');
|
|
807
|
-
// Effect (signal → DOM) — always reactive since it references component variables
|
|
808
|
-
lines.push(` __effect(() => {`);
|
|
809
|
-
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
810
|
-
lines.push(` ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
811
|
-
} else if (mb.prop === 'checked') {
|
|
812
|
-
lines.push(` ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
813
|
-
} else {
|
|
814
|
-
lines.push(` ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
815
|
-
}
|
|
816
|
-
lines.push(` });`);
|
|
817
|
-
// Listener (DOM → signal)
|
|
818
|
-
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
819
|
-
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
820
|
-
} else if (mb.coerce) {
|
|
821
|
-
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
822
|
-
} else {
|
|
823
|
-
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
1043
|
+
generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
826
1044
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
|
|
832
|
-
lines.push(` { const __slotEl = ${slotNodeRef};`);
|
|
833
|
-
lines.push(` const __sp = { ${propsEntries} };`);
|
|
834
|
-
lines.push(` let __h = __slotEl.innerHTML;`);
|
|
835
|
-
lines.push(` for (const [k, v] of Object.entries(__sp)) {`);
|
|
836
|
-
lines.push(` __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
837
|
-
lines.push(` }`);
|
|
838
|
-
lines.push(` __slotEl.innerHTML = __h;`);
|
|
839
|
-
lines.push(` }`);
|
|
840
|
-
}
|
|
1045
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
1046
|
+
lines.push(` this.${vn}_nodes.push(node);`);
|
|
1047
|
+
lines.push(' });');
|
|
1048
|
+
lines.push(' });');
|
|
841
1049
|
}
|
|
842
|
-
|
|
843
|
-
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
844
|
-
lines.push(` this.${vn}_nodes.push(node);`);
|
|
845
|
-
lines.push(' });');
|
|
846
|
-
lines.push(' });');
|
|
847
1050
|
}
|
|
848
1051
|
|
|
849
1052
|
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
850
1053
|
for (const hook of onMountHooks) {
|
|
851
1054
|
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1055
|
+
if (hook.async) {
|
|
1056
|
+
lines.push(' (async () => {');
|
|
1057
|
+
const bodyLines = body.split('\n');
|
|
1058
|
+
for (const line of bodyLines) {
|
|
1059
|
+
lines.push(` ${line}`);
|
|
1060
|
+
}
|
|
1061
|
+
lines.push(' })();');
|
|
1062
|
+
} else {
|
|
1063
|
+
const bodyLines = body.split('\n');
|
|
1064
|
+
for (const line of bodyLines) {
|
|
1065
|
+
lines.push(` ${line}`);
|
|
1066
|
+
}
|
|
855
1067
|
}
|
|
856
1068
|
}
|
|
857
1069
|
|
|
@@ -863,9 +1075,18 @@ export function generateComponent(parseResult) {
|
|
|
863
1075
|
lines.push(' disconnectedCallback() {');
|
|
864
1076
|
for (const hook of onDestroyHooks) {
|
|
865
1077
|
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1078
|
+
if (hook.async) {
|
|
1079
|
+
lines.push(' (async () => {');
|
|
1080
|
+
const bodyLines = body.split('\n');
|
|
1081
|
+
for (const line of bodyLines) {
|
|
1082
|
+
lines.push(` ${line}`);
|
|
1083
|
+
}
|
|
1084
|
+
lines.push(' })();');
|
|
1085
|
+
} else {
|
|
1086
|
+
const bodyLines = body.split('\n');
|
|
1087
|
+
for (const line of bodyLines) {
|
|
1088
|
+
lines.push(` ${line}`);
|
|
1089
|
+
}
|
|
869
1090
|
}
|
|
870
1091
|
}
|
|
871
1092
|
lines.push(' }');
|
|
@@ -978,20 +1199,21 @@ export function generateComponent(parseResult) {
|
|
|
978
1199
|
|
|
979
1200
|
// Events: generate addEventListener
|
|
980
1201
|
for (const e of branch.events) {
|
|
1202
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
981
1203
|
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
982
|
-
lines.push(` ${e.varName}.addEventListener('${e.event}',
|
|
1204
|
+
lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
983
1205
|
}
|
|
984
1206
|
|
|
985
1207
|
// Show bindings: generate effects
|
|
986
1208
|
for (const sb of branch.showBindings) {
|
|
987
|
-
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1209
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
988
1210
|
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
989
1211
|
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
990
1212
|
}
|
|
991
1213
|
|
|
992
1214
|
// Attr bindings: generate effects
|
|
993
1215
|
for (const ab of branch.attrBindings) {
|
|
994
|
-
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
1216
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
995
1217
|
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
996
1218
|
lines.push(` __effect(() => {`);
|
|
997
1219
|
lines.push(` const __val = ${expr};`);
|