@sprlab/wccompiler 0.0.2 → 0.2.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 +268 -51
- package/bin/wcc.js +65 -100
- package/bin/wcc.test.js +119 -0
- package/lib/codegen.js +850 -170
- package/lib/compiler.js +100 -25
- package/lib/config.js +33 -43
- package/lib/css-scoper.js +13 -0
- package/lib/dev-server.js +19 -0
- package/lib/parser.js +1001 -109
- package/lib/printer.js +92 -78
- package/lib/reactive-runtime.js +1 -0
- package/lib/tree-walker.js +682 -43
- package/lib/types.js +192 -0
- package/lib/wcc-runtime.js +26 -0
- package/package.json +14 -9
- package/types/wcc.d.ts +27 -0
- package/types/wcc.test.js +46 -0
package/lib/codegen.js
CHANGED
|
@@ -1,29 +1,298 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Code Generator for
|
|
2
|
+
* Code Generator for wcCompiler v2.
|
|
3
3
|
*
|
|
4
|
-
* Takes a complete ParseResult (with bindings, events
|
|
4
|
+
* Takes a complete ParseResult (with bindings, events populated by tree-walker)
|
|
5
5
|
* and produces a self-contained JavaScript string with:
|
|
6
6
|
* - Inline mini reactive runtime (zero imports)
|
|
7
7
|
* - Scoped CSS injection
|
|
8
|
-
* - HTMLElement class with signals,
|
|
8
|
+
* - HTMLElement class with signals, computeds, effects, events
|
|
9
|
+
* - customElements.define registration
|
|
10
|
+
*
|
|
11
|
+
* This is a simplified version of v1's codegen, scoped to core features only:
|
|
12
|
+
* signals, computeds, effects, text interpolation, event bindings, user methods.
|
|
13
|
+
* No props, emits, slots, if, for, model, show, attr, refs, or lifecycle hooks.
|
|
9
14
|
*/
|
|
10
15
|
|
|
11
16
|
import { reactiveRuntime } from './reactive-runtime.js';
|
|
12
17
|
import { scopeCSS } from './css-scoper.js';
|
|
13
18
|
|
|
19
|
+
/** @import { ParseResult } from './types.js' */
|
|
20
|
+
|
|
14
21
|
/**
|
|
15
22
|
* Convert a path array to a JS expression string.
|
|
16
|
-
* Inlined here to avoid pulling in jsdom via tree-walker.js.
|
|
17
23
|
* e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
|
|
24
|
+
*
|
|
25
|
+
* @param {string[]} parts
|
|
26
|
+
* @param {string} rootVar
|
|
27
|
+
* @returns {string}
|
|
18
28
|
*/
|
|
19
|
-
function pathExpr(parts, rootVar) {
|
|
29
|
+
export function pathExpr(parts, rootVar) {
|
|
20
30
|
return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Escape special regex characters in a string.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} str
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function escapeRegex(str) {
|
|
40
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the signal reference for a slot prop source expression.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} source — Source variable name from :prop="source"
|
|
47
|
+
* @param {string[]} signalNames — Signal variable names
|
|
48
|
+
* @param {string[]} computedNames — Computed variable names
|
|
49
|
+
* @param {Set<string>} propNames — Prop names from defineProps
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function slotPropRef(source, signalNames, computedNames, propNames) {
|
|
53
|
+
if (propNames.has(source)) return `this._s_${source}()`;
|
|
54
|
+
if (computedNames.includes(source)) return `this._c_${source}()`;
|
|
55
|
+
if (signalNames.includes(source)) return `this._${source}()`;
|
|
56
|
+
return `'${source}'`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Transform an expression by rewriting signal/computed variable references
|
|
61
|
+
* to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
|
|
62
|
+
*
|
|
63
|
+
* Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
|
|
64
|
+
* Also handles `emitsObjectName(` → `this._emit(` transformation.
|
|
65
|
+
*
|
|
66
|
+
* Uses word-boundary regex for each known signal/computed name.
|
|
67
|
+
* Does NOT transform if the name is followed by `.set(` (that's a write,
|
|
68
|
+
* handled by transformMethodBody).
|
|
69
|
+
*
|
|
70
|
+
* @param {string} expr — Expression to transform
|
|
71
|
+
* @param {string[]} signalNames — Signal variable names
|
|
72
|
+
* @param {string[]} computedNames — Computed variable names
|
|
73
|
+
* @param {string|null} [propsObjectName] — Props object variable name
|
|
74
|
+
* @param {Set<string>} [propNames] — Set of prop names
|
|
75
|
+
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = []) {
|
|
79
|
+
let result = expr;
|
|
80
|
+
|
|
81
|
+
// Transform emit calls: emitsObjectName( → this._emit(
|
|
82
|
+
if (emitsObjectName) {
|
|
83
|
+
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
84
|
+
result = result.replace(emitsRe, 'this._emit(');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Transform props.x → this._s_x() BEFORE signal/computed transforms
|
|
88
|
+
if (propsObjectName && propNames.size > 0) {
|
|
89
|
+
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
90
|
+
result = result.replace(propsRe, (match, propName) => {
|
|
91
|
+
if (propNames.has(propName)) {
|
|
92
|
+
return `this._s_${propName}()`;
|
|
93
|
+
}
|
|
94
|
+
return match; // leave unknown props unchanged
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Transform computed names first (to avoid partial matches with signals)
|
|
99
|
+
for (const name of computedNames) {
|
|
100
|
+
// Skip propsObjectName and emitsObjectName
|
|
101
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
102
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
103
|
+
// First: transform name() calls → this._c_name() (replace the call, not just the name)
|
|
104
|
+
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
105
|
+
result = result.replace(callRe, `this._c_${name}()`);
|
|
106
|
+
// Then: transform bare name references (not followed by ( or .set() → this._c_name()
|
|
107
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
108
|
+
result = result.replace(bareRe, `this._c_${name}()`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Transform signal names
|
|
112
|
+
for (const name of signalNames) {
|
|
113
|
+
// Skip propsObjectName and emitsObjectName
|
|
114
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
115
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
116
|
+
// First: transform name() calls → this._name() (replace the call, not just the name)
|
|
117
|
+
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
118
|
+
result = result.replace(callRe, `this._${name}()`);
|
|
119
|
+
// Then: transform bare name references (not followed by ( or .set() → this._name()
|
|
120
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
121
|
+
result = result.replace(bareRe, `this._${name}()`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Transform constant names → this._const_name (no function call)
|
|
125
|
+
for (const name of constantNames) {
|
|
126
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
127
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
128
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
129
|
+
result = result.replace(bareRe, `this._const_${name}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Transform a method/effect body by rewriting signal writes and reads.
|
|
137
|
+
*
|
|
138
|
+
* - `emitsObjectName(` → `this._emit(` (emit call)
|
|
139
|
+
* - `props.x` → `this._s_x()` (prop access)
|
|
140
|
+
* - `x.set(value)` → `this._x(value)` (signal write via setter)
|
|
141
|
+
* - `x()` → `this._x()` (signal read)
|
|
142
|
+
* - Computed `x()` → `this._c_x()` (computed read)
|
|
143
|
+
*
|
|
144
|
+
* @param {string} body — Function body to transform
|
|
145
|
+
* @param {string[]} signalNames — Signal variable names
|
|
146
|
+
* @param {string[]} computedNames — Computed variable names
|
|
147
|
+
* @param {string|null} [propsObjectName] — Props object variable name
|
|
148
|
+
* @param {Set<string>} [propNames] — Set of prop names
|
|
149
|
+
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
150
|
+
* @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = []) {
|
|
154
|
+
let result = body;
|
|
155
|
+
|
|
156
|
+
// 0a. Transform emit calls: emitsObjectName( → this._emit(
|
|
157
|
+
if (emitsObjectName) {
|
|
158
|
+
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
159
|
+
result = result.replace(emitsRe, 'this._emit(');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 0b. Transform props.x → this._s_x() BEFORE other transforms
|
|
163
|
+
if (propsObjectName && propNames.size > 0) {
|
|
164
|
+
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
165
|
+
result = result.replace(propsRe, (match, propName) => {
|
|
166
|
+
if (propNames.has(propName)) {
|
|
167
|
+
return `this._s_${propName}()`;
|
|
168
|
+
}
|
|
169
|
+
return match;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 0c. Transform ref access: varName.value → this._varName.value
|
|
174
|
+
for (const name of refVarNames) {
|
|
175
|
+
const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
|
|
176
|
+
result = result.replace(refRe, `this._${name}.value`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 1. Transform signal writes: x.set(value) → this._x(value)
|
|
180
|
+
for (const name of signalNames) {
|
|
181
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
182
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
183
|
+
const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
|
|
184
|
+
result = result.replace(setRe, `this._${name}(`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. Transform computed reads: x() → this._c_x()
|
|
188
|
+
for (const name of computedNames) {
|
|
189
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
190
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
191
|
+
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
192
|
+
result = result.replace(readRe, `this._c_${name}()`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 3. Transform signal reads: x() → this._x()
|
|
196
|
+
for (const name of signalNames) {
|
|
197
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
198
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
199
|
+
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
200
|
+
result = result.replace(readRe, `this._${name}()`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 4. Transform constant reads: name → this._const_name
|
|
204
|
+
for (const name of constantNames) {
|
|
205
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
206
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
207
|
+
const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
|
|
208
|
+
result = result.replace(bareRe, `this._const_${name}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Transform an expression within the scope of an each block.
|
|
216
|
+
* - References to itemVar and indexVar are left UNTRANSFORMED
|
|
217
|
+
* - References to component variables (props, reactive, computed) ARE transformed
|
|
218
|
+
*
|
|
219
|
+
* @param {string} expr - The expression to transform
|
|
220
|
+
* @param {string} itemVar - Name of the iteration variable
|
|
221
|
+
* @param {string | null} indexVar - Name of the index variable
|
|
222
|
+
* @param {Set<string>} propsSet
|
|
223
|
+
* @param {Set<string>} rootVarNames - Set of signal names
|
|
224
|
+
* @param {Set<string>} computedNames
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
228
|
+
let r = expr;
|
|
229
|
+
const excludeSet = new Set([itemVar]);
|
|
230
|
+
if (indexVar) excludeSet.add(indexVar);
|
|
231
|
+
|
|
232
|
+
for (const p of propsSet) {
|
|
233
|
+
if (excludeSet.has(p)) continue;
|
|
234
|
+
r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
|
|
235
|
+
}
|
|
236
|
+
for (const n of rootVarNames) {
|
|
237
|
+
if (excludeSet.has(n)) continue;
|
|
238
|
+
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
|
|
239
|
+
}
|
|
240
|
+
for (const n of computedNames) {
|
|
241
|
+
if (excludeSet.has(n)) continue;
|
|
242
|
+
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
|
|
243
|
+
}
|
|
244
|
+
return r;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if a binding name is static within an each scope (references only item/index).
|
|
249
|
+
* A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
|
|
252
|
+
* @param {string} itemVar
|
|
253
|
+
* @param {string | null} indexVar
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
export function isStaticForBinding(name, itemVar, indexVar) {
|
|
257
|
+
if (name === itemVar || name.startsWith(itemVar + '.')) return true;
|
|
258
|
+
if (indexVar && name === indexVar) return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if an expression is static within an each scope (references only item/index, no component vars).
|
|
264
|
+
*
|
|
265
|
+
* @param {string} expr
|
|
266
|
+
* @param {string} itemVar
|
|
267
|
+
* @param {string | null} indexVar
|
|
268
|
+
* @param {Set<string>} propsSet
|
|
269
|
+
* @param {Set<string>} rootVarNames
|
|
270
|
+
* @param {Set<string>} computedNames
|
|
271
|
+
* @returns {boolean}
|
|
272
|
+
*/
|
|
273
|
+
export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
274
|
+
const excludeSet = new Set([itemVar]);
|
|
275
|
+
if (indexVar) excludeSet.add(indexVar);
|
|
276
|
+
|
|
277
|
+
for (const p of propsSet) {
|
|
278
|
+
if (excludeSet.has(p)) continue;
|
|
279
|
+
if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
|
|
280
|
+
}
|
|
281
|
+
for (const n of rootVarNames) {
|
|
282
|
+
if (excludeSet.has(n)) continue;
|
|
283
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
284
|
+
}
|
|
285
|
+
for (const n of computedNames) {
|
|
286
|
+
if (excludeSet.has(n)) continue;
|
|
287
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
|
|
23
292
|
/**
|
|
24
293
|
* Generate a fully self-contained JS component from a ParseResult.
|
|
25
294
|
*
|
|
26
|
-
* @param {
|
|
295
|
+
* @param {ParseResult} parseResult — Complete IR with bindings/events
|
|
27
296
|
* @returns {string} JavaScript source code
|
|
28
297
|
*/
|
|
29
298
|
export function generateComponent(parseResult) {
|
|
@@ -31,28 +300,43 @@ export function generateComponent(parseResult) {
|
|
|
31
300
|
tagName,
|
|
32
301
|
className,
|
|
33
302
|
style,
|
|
34
|
-
|
|
35
|
-
reactiveVars,
|
|
303
|
+
signals,
|
|
36
304
|
computeds,
|
|
37
|
-
|
|
305
|
+
effects,
|
|
38
306
|
methods,
|
|
39
307
|
bindings,
|
|
40
308
|
events,
|
|
41
|
-
slots,
|
|
42
309
|
processedTemplate,
|
|
310
|
+
propDefs = [],
|
|
311
|
+
propsObjectName = null,
|
|
312
|
+
emits = [],
|
|
313
|
+
emitsObjectName = null,
|
|
314
|
+
ifBlocks = [],
|
|
315
|
+
showBindings = [],
|
|
316
|
+
forBlocks = [],
|
|
317
|
+
onMountHooks = [],
|
|
318
|
+
onDestroyHooks = [],
|
|
319
|
+
modelBindings = [],
|
|
320
|
+
attrBindings = [],
|
|
321
|
+
slots = [],
|
|
322
|
+
constantVars = [],
|
|
323
|
+
refs = [],
|
|
324
|
+
refBindings = [],
|
|
43
325
|
} = parseResult;
|
|
44
326
|
|
|
45
|
-
const
|
|
46
|
-
const computedNames =
|
|
47
|
-
const
|
|
327
|
+
const signalNames = signals.map(s => s.name);
|
|
328
|
+
const computedNames = computeds.map(c => c.name);
|
|
329
|
+
const constantNames = constantVars.map(v => v.name);
|
|
330
|
+
const refVarNames = refs.map(r => r.varName);
|
|
331
|
+
const propNames = new Set(propDefs.map(p => p.name));
|
|
48
332
|
|
|
49
333
|
const lines = [];
|
|
50
334
|
|
|
51
|
-
// ── 1. Inline reactive runtime
|
|
335
|
+
// ── 1. Inline reactive runtime ──
|
|
52
336
|
lines.push(reactiveRuntime.trim());
|
|
53
337
|
lines.push('');
|
|
54
338
|
|
|
55
|
-
// ── 2. CSS injection (
|
|
339
|
+
// ── 2. CSS injection (scoped CSS into document.head, always) ──
|
|
56
340
|
if (style) {
|
|
57
341
|
const scoped = scopeCSS(style, tagName);
|
|
58
342
|
lines.push(`const __css_${className} = document.createElement('style');`);
|
|
@@ -61,25 +345,26 @@ export function generateComponent(parseResult) {
|
|
|
61
345
|
lines.push('');
|
|
62
346
|
}
|
|
63
347
|
|
|
64
|
-
// ── 3. Template ──
|
|
348
|
+
// ── 3. Template element ──
|
|
65
349
|
lines.push(`const __t_${className} = document.createElement('template');`);
|
|
66
|
-
lines.push(`__t_${className}.innerHTML = \`${processedTemplate}\`;`);
|
|
350
|
+
lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
|
|
67
351
|
lines.push('');
|
|
68
352
|
|
|
69
|
-
// ── 4.
|
|
353
|
+
// ── 4. HTMLElement class ──
|
|
70
354
|
lines.push(`class ${className} extends HTMLElement {`);
|
|
71
355
|
|
|
72
|
-
// observedAttributes
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
356
|
+
// Static observedAttributes (if props exist)
|
|
357
|
+
if (propDefs.length > 0) {
|
|
358
|
+
const attrNames = propDefs.map(p => `'${p.attrName}'`).join(', ');
|
|
359
|
+
lines.push(` static get observedAttributes() { return [${attrNames}]; }`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
77
362
|
|
|
78
|
-
//
|
|
363
|
+
// Constructor
|
|
79
364
|
lines.push(' constructor() {');
|
|
80
365
|
lines.push(' super();');
|
|
81
366
|
|
|
82
|
-
// Slot resolution
|
|
367
|
+
// Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
|
|
83
368
|
if (slots.length > 0) {
|
|
84
369
|
lines.push(' const __slotMap = {};');
|
|
85
370
|
lines.push(' const __defaultSlotNodes = [];');
|
|
@@ -97,48 +382,116 @@ export function generateComponent(parseResult) {
|
|
|
97
382
|
lines.push(' }');
|
|
98
383
|
}
|
|
99
384
|
|
|
100
|
-
// Clone template
|
|
385
|
+
// Clone template
|
|
101
386
|
lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
|
|
102
387
|
|
|
103
|
-
|
|
104
|
-
for (const
|
|
105
|
-
lines.push(` this.${
|
|
388
|
+
// Assign DOM refs for bindings
|
|
389
|
+
for (const b of bindings) {
|
|
390
|
+
lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
|
|
106
391
|
}
|
|
107
392
|
|
|
108
|
-
|
|
109
|
-
|
|
393
|
+
// Assign DOM refs for events
|
|
394
|
+
for (const e of events) {
|
|
395
|
+
lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Assign DOM refs for show bindings
|
|
399
|
+
for (const sb of showBindings) {
|
|
400
|
+
lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
|
|
401
|
+
}
|
|
110
402
|
|
|
111
|
-
//
|
|
403
|
+
// Assign DOM refs for model bindings
|
|
404
|
+
for (const mb of modelBindings) {
|
|
405
|
+
lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Assign DOM refs for slot placeholders
|
|
112
409
|
for (const s of slots) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
410
|
+
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Assign DOM refs for attr bindings (reuse ref when same path)
|
|
414
|
+
const attrPathMap = new Map();
|
|
415
|
+
for (const ab of attrBindings) {
|
|
416
|
+
const pathKey = ab.path.join('.');
|
|
417
|
+
if (attrPathMap.has(pathKey)) {
|
|
418
|
+
lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
|
|
119
419
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
420
|
+
lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
|
|
421
|
+
attrPathMap.set(pathKey, ab.varName);
|
|
122
422
|
}
|
|
123
423
|
}
|
|
124
424
|
|
|
125
|
-
//
|
|
126
|
-
for (const p of
|
|
127
|
-
lines.push(` this._s_${p} = __signal(
|
|
425
|
+
// Prop signal initialization (BEFORE user signals)
|
|
426
|
+
for (const p of propDefs) {
|
|
427
|
+
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
128
428
|
}
|
|
129
|
-
|
|
130
|
-
|
|
429
|
+
|
|
430
|
+
// Signal initialization
|
|
431
|
+
for (const s of signals) {
|
|
432
|
+
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
131
433
|
}
|
|
132
434
|
|
|
133
|
-
//
|
|
435
|
+
// Constant initialization
|
|
436
|
+
for (const c of constantVars) {
|
|
437
|
+
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Computed initialization
|
|
134
441
|
for (const c of computeds) {
|
|
135
|
-
const body = transformExpr(c.body,
|
|
442
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
136
443
|
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
137
444
|
}
|
|
138
445
|
|
|
139
|
-
//
|
|
140
|
-
for (const
|
|
141
|
-
|
|
446
|
+
// ── if: template creation, anchor reference, state init ──
|
|
447
|
+
for (const ifBlock of ifBlocks) {
|
|
448
|
+
const vn = ifBlock.varName;
|
|
449
|
+
// Template per branch
|
|
450
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
451
|
+
const branch = ifBlock.branches[i];
|
|
452
|
+
lines.push(` this.${vn}_t${i} = document.createElement('template');`);
|
|
453
|
+
lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
454
|
+
}
|
|
455
|
+
// Reference to anchor comment node (must be before appendChild moves nodes out of __root)
|
|
456
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
|
|
457
|
+
// Active branch state
|
|
458
|
+
lines.push(` this.${vn}_current = null;`);
|
|
459
|
+
lines.push(` this.${vn}_active = undefined;`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ── each: template creation, anchor reference, nodes array ──
|
|
463
|
+
for (const forBlock of forBlocks) {
|
|
464
|
+
const vn = forBlock.varName;
|
|
465
|
+
lines.push(` this.${vn}_tpl = document.createElement('template');`);
|
|
466
|
+
lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
|
|
467
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
|
|
468
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
472
|
+
for (const rb of refBindings) {
|
|
473
|
+
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Append DOM (always light DOM)
|
|
477
|
+
lines.push(" this.innerHTML = '';");
|
|
478
|
+
lines.push(' this.appendChild(__root);');
|
|
479
|
+
|
|
480
|
+
// Static slot injection (after DOM is appended)
|
|
481
|
+
for (const s of slots) {
|
|
482
|
+
if (s.name && s.slotProps.length > 0) {
|
|
483
|
+
// Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
|
|
484
|
+
lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
|
|
485
|
+
if (s.defaultContent) {
|
|
486
|
+
lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
|
|
487
|
+
}
|
|
488
|
+
} else if (s.name) {
|
|
489
|
+
// Named slot: inject content directly
|
|
490
|
+
lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
|
|
491
|
+
} else {
|
|
492
|
+
// Default slot: inject collected child nodes
|
|
493
|
+
lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
494
|
+
}
|
|
142
495
|
}
|
|
143
496
|
|
|
144
497
|
lines.push(' }');
|
|
@@ -147,19 +500,35 @@ export function generateComponent(parseResult) {
|
|
|
147
500
|
// connectedCallback
|
|
148
501
|
lines.push(' connectedCallback() {');
|
|
149
502
|
|
|
150
|
-
// Binding effects
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
lines.push(` this.${b.varName}.textContent = ${
|
|
503
|
+
// Binding effects — one __effect per binding
|
|
504
|
+
for (const b of bindings) {
|
|
505
|
+
if (b.type === 'prop') {
|
|
506
|
+
lines.push(' __effect(() => {');
|
|
507
|
+
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
508
|
+
lines.push(' });');
|
|
509
|
+
} else if (b.type === 'signal') {
|
|
510
|
+
lines.push(' __effect(() => {');
|
|
511
|
+
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
512
|
+
lines.push(' });');
|
|
513
|
+
} else if (b.type === 'computed') {
|
|
514
|
+
lines.push(' __effect(() => {');
|
|
515
|
+
lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
|
|
516
|
+
lines.push(' });');
|
|
517
|
+
} else {
|
|
518
|
+
// method type — call the method
|
|
519
|
+
lines.push(' __effect(() => {');
|
|
520
|
+
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
521
|
+
lines.push(' });');
|
|
155
522
|
}
|
|
156
|
-
lines.push(' });');
|
|
157
523
|
}
|
|
158
524
|
|
|
159
|
-
//
|
|
525
|
+
// Scoped slot effects — reactive resolution of {{propName}} in consumer templates
|
|
160
526
|
for (const s of slots) {
|
|
161
527
|
if (s.name && s.slotProps.length > 0) {
|
|
162
|
-
const propsObj = s.slotProps.map(sp =>
|
|
528
|
+
const propsObj = s.slotProps.map(sp => {
|
|
529
|
+
const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
|
|
530
|
+
return `${sp.prop}: ${ref}`;
|
|
531
|
+
}).join(', ');
|
|
163
532
|
lines.push(` if (this.__slotTpl_${s.name}) {`);
|
|
164
533
|
lines.push(' __effect(() => {');
|
|
165
534
|
lines.push(` const __props = { ${propsObj} };`);
|
|
@@ -173,152 +542,463 @@ export function generateComponent(parseResult) {
|
|
|
173
542
|
}
|
|
174
543
|
}
|
|
175
544
|
|
|
176
|
-
//
|
|
177
|
-
for (const
|
|
178
|
-
const
|
|
179
|
-
let body = transformExpr(w.body, propsSet, rootVarNames, computedNames);
|
|
180
|
-
body = body.replace(/\bemit\(/g, 'this._emit(');
|
|
545
|
+
// User effects
|
|
546
|
+
for (const eff of effects) {
|
|
547
|
+
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
181
548
|
lines.push(' __effect(() => {');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
549
|
+
// Indent each line of the body
|
|
550
|
+
const bodyLines = body.split('\n');
|
|
551
|
+
for (const line of bodyLines) {
|
|
552
|
+
lines.push(` ${line}`);
|
|
553
|
+
}
|
|
188
554
|
lines.push(' });');
|
|
189
555
|
}
|
|
190
556
|
|
|
191
|
-
// Event listeners
|
|
557
|
+
// Event listeners
|
|
192
558
|
for (const e of events) {
|
|
193
559
|
lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
194
560
|
}
|
|
195
561
|
|
|
196
|
-
|
|
197
|
-
|
|
562
|
+
// Show effects — one __effect per ShowBinding
|
|
563
|
+
for (const sb of showBindings) {
|
|
564
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
565
|
+
lines.push(' __effect(() => {');
|
|
566
|
+
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
567
|
+
lines.push(' });');
|
|
568
|
+
}
|
|
198
569
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
570
|
+
// Model effects — signal → DOM (one __effect per ModelBinding)
|
|
571
|
+
for (const mb of modelBindings) {
|
|
572
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
573
|
+
// Radio: compare signal value to radioValue
|
|
574
|
+
lines.push(' __effect(() => {');
|
|
575
|
+
lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
576
|
+
lines.push(' });');
|
|
577
|
+
} else if (mb.prop === 'checked') {
|
|
578
|
+
// Checkbox: coerce to boolean
|
|
579
|
+
lines.push(' __effect(() => {');
|
|
580
|
+
lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
581
|
+
lines.push(' });');
|
|
582
|
+
} else {
|
|
583
|
+
// Value-based (text, number, textarea, select): nullish coalesce to ''
|
|
584
|
+
lines.push(' __effect(() => {');
|
|
585
|
+
lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
586
|
+
lines.push(' });');
|
|
587
|
+
}
|
|
203
588
|
}
|
|
204
|
-
lines.push(' }');
|
|
205
|
-
lines.push('');
|
|
206
589
|
|
|
207
|
-
//
|
|
208
|
-
for (const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
590
|
+
// Model event listeners — DOM → signal (one addEventListener per ModelBinding)
|
|
591
|
+
for (const mb of modelBindings) {
|
|
592
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
593
|
+
// Checkbox: read e.target.checked
|
|
594
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
595
|
+
} else if (mb.coerce) {
|
|
596
|
+
// Number input: wrap in Number()
|
|
597
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
598
|
+
} else {
|
|
599
|
+
// All others: read e.target.value
|
|
600
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
601
|
+
}
|
|
212
602
|
}
|
|
213
603
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
604
|
+
// Attr binding effects — one __effect per AttrBinding
|
|
605
|
+
for (const ab of attrBindings) {
|
|
606
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
607
|
+
if (ab.kind === 'attr') {
|
|
608
|
+
lines.push(' __effect(() => {');
|
|
609
|
+
lines.push(` const __v = ${expr};`);
|
|
610
|
+
lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
|
|
611
|
+
lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
612
|
+
lines.push(' });');
|
|
613
|
+
} else if (ab.kind === 'bool') {
|
|
614
|
+
lines.push(' __effect(() => {');
|
|
615
|
+
lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
|
|
616
|
+
lines.push(' });');
|
|
617
|
+
} else if (ab.kind === 'class') {
|
|
618
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
619
|
+
// Object expression: iterate entries, classList.add/remove
|
|
620
|
+
lines.push(' __effect(() => {');
|
|
621
|
+
lines.push(` const __obj = ${expr};`);
|
|
622
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
623
|
+
lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
|
|
624
|
+
lines.push(' }');
|
|
625
|
+
lines.push(' });');
|
|
626
|
+
} else {
|
|
627
|
+
// String expression: set className
|
|
628
|
+
lines.push(' __effect(() => {');
|
|
629
|
+
lines.push(` this.${ab.varName}.className = ${expr};`);
|
|
630
|
+
lines.push(' });');
|
|
631
|
+
}
|
|
632
|
+
} else if (ab.kind === 'style') {
|
|
633
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
634
|
+
// Object expression: iterate entries, set style[key]
|
|
635
|
+
lines.push(' __effect(() => {');
|
|
636
|
+
lines.push(` const __obj = ${expr};`);
|
|
637
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
638
|
+
lines.push(` this.${ab.varName}.style[__k] = __val;`);
|
|
639
|
+
lines.push(' }');
|
|
640
|
+
lines.push(' });');
|
|
641
|
+
} else {
|
|
642
|
+
// String expression: set cssText
|
|
643
|
+
lines.push(' __effect(() => {');
|
|
644
|
+
lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
|
|
645
|
+
lines.push(' });');
|
|
646
|
+
}
|
|
647
|
+
}
|
|
220
648
|
}
|
|
221
649
|
|
|
222
|
-
//
|
|
223
|
-
for (const
|
|
224
|
-
|
|
225
|
-
lines.push(
|
|
226
|
-
lines.push(
|
|
227
|
-
|
|
228
|
-
|
|
650
|
+
// ── if effects ──
|
|
651
|
+
for (const ifBlock of ifBlocks) {
|
|
652
|
+
const vn = ifBlock.varName;
|
|
653
|
+
lines.push(' __effect(() => {');
|
|
654
|
+
lines.push(' let __branch = null;');
|
|
655
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
656
|
+
const branch = ifBlock.branches[i];
|
|
657
|
+
if (branch.type === 'if') {
|
|
658
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
659
|
+
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
660
|
+
} else if (branch.type === 'else-if') {
|
|
661
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
662
|
+
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
663
|
+
} else {
|
|
664
|
+
// else
|
|
665
|
+
lines.push(` else { __branch = ${i}; }`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
lines.push(` if (__branch === this.${vn}_active) return;`);
|
|
669
|
+
// Remove previous branch
|
|
670
|
+
lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
|
|
671
|
+
// Insert new branch
|
|
672
|
+
lines.push(' if (__branch !== null) {');
|
|
673
|
+
const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
|
|
674
|
+
lines.push(` const tpl = [${tplArray}][__branch];`);
|
|
675
|
+
lines.push(' const clone = tpl.content.cloneNode(true);');
|
|
676
|
+
lines.push(' const node = clone.firstChild;');
|
|
677
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
678
|
+
lines.push(` this.${vn}_current = node;`);
|
|
679
|
+
// Setup bindings/events for active branch (only if any branch has bindings/events)
|
|
680
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
681
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
682
|
+
(b.events && b.events.length > 0) ||
|
|
683
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
684
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
685
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
686
|
+
);
|
|
687
|
+
if (hasSetup) {
|
|
688
|
+
lines.push(` this.${vn}_setup(node, __branch);`);
|
|
689
|
+
}
|
|
690
|
+
lines.push(' }');
|
|
691
|
+
lines.push(` this.${vn}_active = __branch;`);
|
|
692
|
+
lines.push(' });');
|
|
229
693
|
}
|
|
230
694
|
|
|
231
|
-
|
|
232
|
-
|
|
695
|
+
// ── each effects ──
|
|
696
|
+
for (const forBlock of forBlocks) {
|
|
697
|
+
const vn = forBlock.varName;
|
|
698
|
+
const { itemVar, indexVar, source } = forBlock;
|
|
233
699
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
lines.push('');
|
|
700
|
+
const signalNamesSet = new Set(signalNames);
|
|
701
|
+
const computedNamesSet = new Set(computedNames);
|
|
237
702
|
|
|
238
|
-
|
|
239
|
-
|
|
703
|
+
// Transform the source expression
|
|
704
|
+
const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
240
705
|
|
|
241
|
-
|
|
706
|
+
lines.push(' __effect(() => {');
|
|
707
|
+
lines.push(` const __source = ${sourceExpr};`);
|
|
708
|
+
lines.push('');
|
|
709
|
+
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
710
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
711
|
+
lines.push('');
|
|
712
|
+
lines.push(" const __iter = typeof __source === 'number'");
|
|
713
|
+
lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
|
|
714
|
+
lines.push(' : (__source || []);');
|
|
715
|
+
lines.push('');
|
|
716
|
+
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
717
|
+
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
718
|
+
lines.push(' const node = clone.firstChild;');
|
|
719
|
+
|
|
720
|
+
// Setup bindings per item
|
|
721
|
+
for (const b of forBlock.bindings) {
|
|
722
|
+
const nodeRef = pathExpr(b.path, 'node');
|
|
723
|
+
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
724
|
+
// Static binding: reference only item/index, assign once
|
|
725
|
+
lines.push(` ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
726
|
+
} else {
|
|
727
|
+
// Reactive binding: references component variables, wrap in effect
|
|
728
|
+
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
729
|
+
lines.push(` __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
242
732
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
733
|
+
// Setup events per item
|
|
734
|
+
for (const e of forBlock.events) {
|
|
735
|
+
const nodeRef = pathExpr(e.path, 'node');
|
|
736
|
+
lines.push(` ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Setup show per item
|
|
740
|
+
for (const sb of forBlock.showBindings) {
|
|
741
|
+
const nodeRef = pathExpr(sb.path, 'node');
|
|
742
|
+
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
743
|
+
const expr = sb.expression;
|
|
744
|
+
lines.push(` ${nodeRef}.style.display = (${expr}) ? '' : 'none';`);
|
|
745
|
+
} else {
|
|
746
|
+
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
747
|
+
lines.push(` __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Setup attr bindings per item
|
|
752
|
+
for (const ab of forBlock.attrBindings) {
|
|
753
|
+
const nodeRef = pathExpr(ab.path, 'node');
|
|
754
|
+
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
755
|
+
const expr = ab.expression;
|
|
756
|
+
lines.push(` const __val_${ab.varName} = ${expr};`);
|
|
757
|
+
lines.push(` if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
758
|
+
} else {
|
|
759
|
+
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
760
|
+
lines.push(` __effect(() => {`);
|
|
761
|
+
lines.push(` const __val = ${expr};`);
|
|
762
|
+
lines.push(` if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
763
|
+
lines.push(` else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
764
|
+
lines.push(` });`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Setup model bindings per item
|
|
769
|
+
for (const mb of (forBlock.modelBindings || [])) {
|
|
770
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
771
|
+
// Effect (signal → DOM) — always reactive since it references component variables
|
|
772
|
+
lines.push(` __effect(() => {`);
|
|
773
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
774
|
+
lines.push(` ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
775
|
+
} else if (mb.prop === 'checked') {
|
|
776
|
+
lines.push(` ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
777
|
+
} else {
|
|
778
|
+
lines.push(` ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
779
|
+
}
|
|
780
|
+
lines.push(` });`);
|
|
781
|
+
// Listener (DOM → signal)
|
|
782
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
783
|
+
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
784
|
+
} else if (mb.coerce) {
|
|
785
|
+
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
786
|
+
} else {
|
|
787
|
+
lines.push(` ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Setup scoped slot resolution per item
|
|
792
|
+
for (const s of (forBlock.slots || [])) {
|
|
793
|
+
if (s.slotProps.length > 0) {
|
|
794
|
+
const slotNodeRef = pathExpr(s.path, 'node');
|
|
795
|
+
const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
|
|
796
|
+
lines.push(` { const __slotEl = ${slotNodeRef};`);
|
|
797
|
+
lines.push(` const __sp = { ${propsEntries} };`);
|
|
798
|
+
lines.push(` let __h = __slotEl.innerHTML;`);
|
|
799
|
+
lines.push(` for (const [k, v] of Object.entries(__sp)) {`);
|
|
800
|
+
lines.push(` __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
801
|
+
lines.push(` }`);
|
|
802
|
+
lines.push(` __slotEl.innerHTML = __h;`);
|
|
803
|
+
lines.push(` }`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
808
|
+
lines.push(` this.${vn}_nodes.push(node);`);
|
|
809
|
+
lines.push(' });');
|
|
810
|
+
lines.push(' });');
|
|
254
811
|
}
|
|
255
|
-
|
|
256
|
-
|
|
812
|
+
|
|
813
|
+
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
814
|
+
for (const hook of onMountHooks) {
|
|
815
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
816
|
+
const bodyLines = body.split('\n');
|
|
817
|
+
for (const line of bodyLines) {
|
|
818
|
+
lines.push(` ${line}`);
|
|
819
|
+
}
|
|
257
820
|
}
|
|
258
|
-
return r;
|
|
259
|
-
}
|
|
260
821
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
*/
|
|
264
|
-
function transformMethodBody(body, propsSet, rootVarNames, computedNames) {
|
|
265
|
-
let r = body;
|
|
822
|
+
lines.push(' }');
|
|
823
|
+
lines.push('');
|
|
266
824
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
825
|
+
// disconnectedCallback (only when destroy hooks exist)
|
|
826
|
+
if (onDestroyHooks.length > 0) {
|
|
827
|
+
lines.push(' disconnectedCallback() {');
|
|
828
|
+
for (const hook of onDestroyHooks) {
|
|
829
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
830
|
+
const bodyLines = body.split('\n');
|
|
831
|
+
for (const line of bodyLines) {
|
|
832
|
+
lines.push(` ${line}`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
lines.push(' }');
|
|
836
|
+
lines.push('');
|
|
271
837
|
}
|
|
272
838
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
839
|
+
// attributeChangedCallback (if props exist)
|
|
840
|
+
if (propDefs.length > 0) {
|
|
841
|
+
lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
|
|
842
|
+
for (const p of propDefs) {
|
|
843
|
+
const defaultVal = p.default;
|
|
844
|
+
let updateExpr;
|
|
845
|
+
|
|
846
|
+
if (defaultVal === 'true' || defaultVal === 'false') {
|
|
847
|
+
// Boolean coercion: attribute presence = true
|
|
848
|
+
updateExpr = `this._s_${p.name}(newVal != null)`;
|
|
849
|
+
} else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
|
|
850
|
+
// Number coercion
|
|
851
|
+
updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
|
|
852
|
+
} else if (defaultVal === 'undefined') {
|
|
853
|
+
// Undefined default — pass through
|
|
854
|
+
updateExpr = `this._s_${p.name}(newVal)`;
|
|
855
|
+
} else {
|
|
856
|
+
// String default — use nullish coalescing
|
|
857
|
+
updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
|
|
861
|
+
}
|
|
862
|
+
lines.push(' }');
|
|
863
|
+
lines.push('');
|
|
864
|
+
|
|
865
|
+
// Public getters and setters
|
|
866
|
+
for (const p of propDefs) {
|
|
867
|
+
lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
|
|
868
|
+
lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
|
|
869
|
+
lines.push('');
|
|
870
|
+
}
|
|
276
871
|
}
|
|
277
|
-
|
|
278
|
-
|
|
872
|
+
|
|
873
|
+
// _emit method (if emits declared)
|
|
874
|
+
if (emits.length > 0) {
|
|
875
|
+
lines.push(' _emit(name, detail) {');
|
|
876
|
+
lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
|
|
877
|
+
lines.push(' }');
|
|
878
|
+
lines.push('');
|
|
279
879
|
}
|
|
280
|
-
|
|
281
|
-
|
|
880
|
+
|
|
881
|
+
// User methods (prefixed with _)
|
|
882
|
+
for (const m of methods) {
|
|
883
|
+
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
884
|
+
lines.push(` _${m.name}(${m.params}) {`);
|
|
885
|
+
const bodyLines = body.split('\n');
|
|
886
|
+
for (const line of bodyLines) {
|
|
887
|
+
lines.push(` ${line}`);
|
|
888
|
+
}
|
|
889
|
+
lines.push(' }');
|
|
890
|
+
lines.push('');
|
|
282
891
|
}
|
|
283
892
|
|
|
284
|
-
//
|
|
285
|
-
|
|
893
|
+
// ── Ref getter properties ──
|
|
894
|
+
for (const rd of refs) {
|
|
895
|
+
// Find matching RefBinding
|
|
896
|
+
const rb = refBindings.find(b => b.refName === rd.refName);
|
|
897
|
+
if (rb) {
|
|
898
|
+
lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
|
|
899
|
+
lines.push('');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
286
902
|
|
|
287
|
-
|
|
288
|
-
|
|
903
|
+
// ── if setup methods ──
|
|
904
|
+
for (const ifBlock of ifBlocks) {
|
|
905
|
+
const vn = ifBlock.varName;
|
|
906
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
907
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
908
|
+
(b.events && b.events.length > 0) ||
|
|
909
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
910
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
911
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
912
|
+
);
|
|
913
|
+
if (!hasSetup) continue;
|
|
914
|
+
|
|
915
|
+
lines.push(` ${vn}_setup(node, branch) {`);
|
|
916
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
917
|
+
const branch = ifBlock.branches[i];
|
|
918
|
+
const hasBranchSetup =
|
|
919
|
+
(branch.bindings && branch.bindings.length > 0) ||
|
|
920
|
+
(branch.events && branch.events.length > 0) ||
|
|
921
|
+
(branch.showBindings && branch.showBindings.length > 0) ||
|
|
922
|
+
(branch.attrBindings && branch.attrBindings.length > 0) ||
|
|
923
|
+
(branch.modelBindings && branch.modelBindings.length > 0);
|
|
924
|
+
if (!hasBranchSetup) continue;
|
|
925
|
+
|
|
926
|
+
const keyword = i === 0 ? 'if' : 'else if';
|
|
927
|
+
lines.push(` ${keyword} (branch === ${i}) {`);
|
|
928
|
+
|
|
929
|
+
// Bindings: generate DOM refs and effects for text bindings
|
|
930
|
+
for (const b of branch.bindings) {
|
|
931
|
+
lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
|
|
932
|
+
if (b.type === 'prop') {
|
|
933
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
|
|
934
|
+
} else if (b.type === 'signal') {
|
|
935
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
|
|
936
|
+
} else if (b.type === 'computed') {
|
|
937
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
|
|
938
|
+
} else {
|
|
939
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Events: generate addEventListener
|
|
944
|
+
for (const e of branch.events) {
|
|
945
|
+
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
946
|
+
lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Show bindings: generate effects
|
|
950
|
+
for (const sb of branch.showBindings) {
|
|
951
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
952
|
+
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
953
|
+
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Attr bindings: generate effects
|
|
957
|
+
for (const ab of branch.attrBindings) {
|
|
958
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
959
|
+
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
960
|
+
lines.push(` __effect(() => {`);
|
|
961
|
+
lines.push(` const __val = ${expr};`);
|
|
962
|
+
lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
963
|
+
lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
|
|
964
|
+
lines.push(` });`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Model bindings: generate effects and listeners
|
|
968
|
+
for (const mb of (branch.modelBindings || [])) {
|
|
969
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
970
|
+
lines.push(` const ${mb.varName} = ${nodeRef};`);
|
|
971
|
+
// Effect (signal → DOM)
|
|
972
|
+
lines.push(` __effect(() => {`);
|
|
973
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
974
|
+
lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
975
|
+
} else if (mb.prop === 'checked') {
|
|
976
|
+
lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
977
|
+
} else {
|
|
978
|
+
lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
979
|
+
}
|
|
980
|
+
lines.push(` });`);
|
|
981
|
+
// Listener (DOM → signal)
|
|
982
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
983
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
984
|
+
} else if (mb.coerce) {
|
|
985
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
986
|
+
} else {
|
|
987
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
289
990
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (b.type === 'computed') return `this._c_${b.name}()`;
|
|
296
|
-
return `this._${b.name}()`;
|
|
297
|
-
}
|
|
991
|
+
lines.push(' }');
|
|
992
|
+
}
|
|
993
|
+
lines.push(' }');
|
|
994
|
+
lines.push('');
|
|
995
|
+
}
|
|
298
996
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
*/
|
|
302
|
-
function signalRef(name, propsSet, computedNames, rootVarNames) {
|
|
303
|
-
if (propsSet.has(name)) return `this._s_${name}()`;
|
|
304
|
-
if (computedNames.has(name)) return `this._c_${name}()`;
|
|
305
|
-
if (rootVarNames.has(name)) return `this._${name}()`;
|
|
306
|
-
return `this._${name}()`;
|
|
307
|
-
}
|
|
997
|
+
lines.push('}');
|
|
998
|
+
lines.push('');
|
|
308
999
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
*/
|
|
312
|
-
function slotPropRef(source, propsSet, computedNames, rootVarNames) {
|
|
313
|
-
if (propsSet.has(source)) return `this._s_${source}()`;
|
|
314
|
-
if (computedNames.has(source)) return `this._c_${source}()`;
|
|
315
|
-
if (rootVarNames.has(source)) return `this._${source}()`;
|
|
316
|
-
return `'${source}'`;
|
|
317
|
-
}
|
|
1000
|
+
// ── 5. Custom element registration ──
|
|
1001
|
+
lines.push(`customElements.define('${tagName}', ${className});`);
|
|
318
1002
|
|
|
319
|
-
|
|
320
|
-
* Check if any method body contains an emit() call.
|
|
321
|
-
*/
|
|
322
|
-
function hasEmitCall(methods) {
|
|
323
|
-
return methods.some(m => /\bemit\(/.test(m.body));
|
|
1003
|
+
return lines.join('\n');
|
|
324
1004
|
}
|