@sprlab/wccompiler 0.0.3 → 0.2.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/README.md +268 -51
- package/bin/wcc.js +65 -100
- package/bin/wcc.test.js +119 -0
- package/lib/codegen.js +882 -166
- package/lib/compiler.js +172 -33
- 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 +721 -43
- package/lib/types.js +215 -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,53 @@ 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 = [],
|
|
325
|
+
childComponents = [],
|
|
326
|
+
childImports = [],
|
|
43
327
|
} = parseResult;
|
|
44
328
|
|
|
45
|
-
const
|
|
46
|
-
const computedNames =
|
|
47
|
-
const
|
|
329
|
+
const signalNames = signals.map(s => s.name);
|
|
330
|
+
const computedNames = computeds.map(c => c.name);
|
|
331
|
+
const constantNames = constantVars.map(v => v.name);
|
|
332
|
+
const refVarNames = refs.map(r => r.varName);
|
|
333
|
+
const propNames = new Set(propDefs.map(p => p.name));
|
|
48
334
|
|
|
49
335
|
const lines = [];
|
|
50
336
|
|
|
51
|
-
// ── 1. Inline reactive runtime
|
|
337
|
+
// ── 1. Inline reactive runtime ──
|
|
52
338
|
lines.push(reactiveRuntime.trim());
|
|
53
339
|
lines.push('');
|
|
54
340
|
|
|
55
|
-
// ──
|
|
341
|
+
// ── 1b. Child component imports ──
|
|
342
|
+
for (const ci of childImports) {
|
|
343
|
+
lines.push(`import '${ci.importPath}';`);
|
|
344
|
+
}
|
|
345
|
+
if (childImports.length > 0) {
|
|
346
|
+
lines.push('');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── 2. CSS injection (scoped CSS into document.head, always) ──
|
|
56
350
|
if (style) {
|
|
57
351
|
const scoped = scopeCSS(style, tagName);
|
|
58
352
|
lines.push(`const __css_${className} = document.createElement('style');`);
|
|
@@ -61,25 +355,26 @@ export function generateComponent(parseResult) {
|
|
|
61
355
|
lines.push('');
|
|
62
356
|
}
|
|
63
357
|
|
|
64
|
-
// ── 3. Template ──
|
|
358
|
+
// ── 3. Template element ──
|
|
65
359
|
lines.push(`const __t_${className} = document.createElement('template');`);
|
|
66
|
-
lines.push(`__t_${className}.innerHTML = \`${processedTemplate}\`;`);
|
|
360
|
+
lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
|
|
67
361
|
lines.push('');
|
|
68
362
|
|
|
69
|
-
// ── 4.
|
|
363
|
+
// ── 4. HTMLElement class ──
|
|
70
364
|
lines.push(`class ${className} extends HTMLElement {`);
|
|
71
365
|
|
|
72
|
-
// observedAttributes
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
366
|
+
// Static observedAttributes (if props exist)
|
|
367
|
+
if (propDefs.length > 0) {
|
|
368
|
+
const attrNames = propDefs.map(p => `'${p.attrName}'`).join(', ');
|
|
369
|
+
lines.push(` static get observedAttributes() { return [${attrNames}]; }`);
|
|
370
|
+
lines.push('');
|
|
371
|
+
}
|
|
77
372
|
|
|
78
|
-
//
|
|
373
|
+
// Constructor
|
|
79
374
|
lines.push(' constructor() {');
|
|
80
375
|
lines.push(' super();');
|
|
81
376
|
|
|
82
|
-
// Slot resolution
|
|
377
|
+
// Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
|
|
83
378
|
if (slots.length > 0) {
|
|
84
379
|
lines.push(' const __slotMap = {};');
|
|
85
380
|
lines.push(' const __defaultSlotNodes = [];');
|
|
@@ -97,48 +392,121 @@ export function generateComponent(parseResult) {
|
|
|
97
392
|
lines.push(' }');
|
|
98
393
|
}
|
|
99
394
|
|
|
100
|
-
// Clone template
|
|
395
|
+
// Clone template
|
|
101
396
|
lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
|
|
102
397
|
|
|
103
|
-
|
|
104
|
-
for (const
|
|
105
|
-
lines.push(` this.${
|
|
398
|
+
// Assign DOM refs for bindings
|
|
399
|
+
for (const b of bindings) {
|
|
400
|
+
lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
|
|
106
401
|
}
|
|
107
402
|
|
|
108
|
-
|
|
109
|
-
|
|
403
|
+
// Assign DOM refs for events
|
|
404
|
+
for (const e of events) {
|
|
405
|
+
lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Assign DOM refs for show bindings
|
|
409
|
+
for (const sb of showBindings) {
|
|
410
|
+
lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Assign DOM refs for model bindings
|
|
414
|
+
for (const mb of modelBindings) {
|
|
415
|
+
lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
|
|
416
|
+
}
|
|
110
417
|
|
|
111
|
-
//
|
|
418
|
+
// Assign DOM refs for slot placeholders
|
|
112
419
|
for (const s of slots) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
420
|
+
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Assign DOM refs for child component instances
|
|
424
|
+
for (const cc of childComponents) {
|
|
425
|
+
lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Assign DOM refs for attr bindings (reuse ref when same path)
|
|
429
|
+
const attrPathMap = new Map();
|
|
430
|
+
for (const ab of attrBindings) {
|
|
431
|
+
const pathKey = ab.path.join('.');
|
|
432
|
+
if (attrPathMap.has(pathKey)) {
|
|
433
|
+
lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
|
|
119
434
|
} else {
|
|
120
|
-
|
|
121
|
-
|
|
435
|
+
lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
|
|
436
|
+
attrPathMap.set(pathKey, ab.varName);
|
|
122
437
|
}
|
|
123
438
|
}
|
|
124
439
|
|
|
125
|
-
//
|
|
126
|
-
for (const p of
|
|
127
|
-
lines.push(` this._s_${p} = __signal(
|
|
440
|
+
// Prop signal initialization (BEFORE user signals)
|
|
441
|
+
for (const p of propDefs) {
|
|
442
|
+
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Signal initialization
|
|
446
|
+
for (const s of signals) {
|
|
447
|
+
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
128
448
|
}
|
|
129
|
-
|
|
130
|
-
|
|
449
|
+
|
|
450
|
+
// Constant initialization
|
|
451
|
+
for (const c of constantVars) {
|
|
452
|
+
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
131
453
|
}
|
|
132
454
|
|
|
133
|
-
// Computed
|
|
455
|
+
// Computed initialization
|
|
134
456
|
for (const c of computeds) {
|
|
135
|
-
const body = transformExpr(c.body,
|
|
457
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
136
458
|
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
137
459
|
}
|
|
138
460
|
|
|
139
|
-
//
|
|
140
|
-
for (const
|
|
141
|
-
|
|
461
|
+
// ── if: template creation, anchor reference, state init ──
|
|
462
|
+
for (const ifBlock of ifBlocks) {
|
|
463
|
+
const vn = ifBlock.varName;
|
|
464
|
+
// Template per branch
|
|
465
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
466
|
+
const branch = ifBlock.branches[i];
|
|
467
|
+
lines.push(` this.${vn}_t${i} = document.createElement('template');`);
|
|
468
|
+
lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
469
|
+
}
|
|
470
|
+
// Reference to anchor comment node (must be before appendChild moves nodes out of __root)
|
|
471
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
|
|
472
|
+
// Active branch state
|
|
473
|
+
lines.push(` this.${vn}_current = null;`);
|
|
474
|
+
lines.push(` this.${vn}_active = undefined;`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── each: template creation, anchor reference, nodes array ──
|
|
478
|
+
for (const forBlock of forBlocks) {
|
|
479
|
+
const vn = forBlock.varName;
|
|
480
|
+
lines.push(` this.${vn}_tpl = document.createElement('template');`);
|
|
481
|
+
lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
|
|
482
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
|
|
483
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
487
|
+
for (const rb of refBindings) {
|
|
488
|
+
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Append DOM (always light DOM)
|
|
492
|
+
lines.push(" this.innerHTML = '';");
|
|
493
|
+
lines.push(' this.appendChild(__root);');
|
|
494
|
+
|
|
495
|
+
// Static slot injection (after DOM is appended)
|
|
496
|
+
for (const s of slots) {
|
|
497
|
+
if (s.name && s.slotProps.length > 0) {
|
|
498
|
+
// Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
|
|
499
|
+
lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
|
|
500
|
+
if (s.defaultContent) {
|
|
501
|
+
lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
|
|
502
|
+
}
|
|
503
|
+
} else if (s.name) {
|
|
504
|
+
// Named slot: inject content directly
|
|
505
|
+
lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
|
|
506
|
+
} else {
|
|
507
|
+
// Default slot: inject collected child nodes
|
|
508
|
+
lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
509
|
+
}
|
|
142
510
|
}
|
|
143
511
|
|
|
144
512
|
lines.push(' }');
|
|
@@ -147,19 +515,35 @@ export function generateComponent(parseResult) {
|
|
|
147
515
|
// connectedCallback
|
|
148
516
|
lines.push(' connectedCallback() {');
|
|
149
517
|
|
|
150
|
-
// Binding effects
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
lines.push(` this.${b.varName}.textContent = ${
|
|
518
|
+
// Binding effects — one __effect per binding
|
|
519
|
+
for (const b of bindings) {
|
|
520
|
+
if (b.type === 'prop') {
|
|
521
|
+
lines.push(' __effect(() => {');
|
|
522
|
+
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
523
|
+
lines.push(' });');
|
|
524
|
+
} else if (b.type === 'signal') {
|
|
525
|
+
lines.push(' __effect(() => {');
|
|
526
|
+
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
527
|
+
lines.push(' });');
|
|
528
|
+
} else if (b.type === 'computed') {
|
|
529
|
+
lines.push(' __effect(() => {');
|
|
530
|
+
lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
|
|
531
|
+
lines.push(' });');
|
|
532
|
+
} else {
|
|
533
|
+
// method type — call the method
|
|
534
|
+
lines.push(' __effect(() => {');
|
|
535
|
+
lines.push(` this.${b.varName}.textContent = this._${b.name}() ?? '';`);
|
|
536
|
+
lines.push(' });');
|
|
155
537
|
}
|
|
156
|
-
lines.push(' });');
|
|
157
538
|
}
|
|
158
539
|
|
|
159
|
-
//
|
|
540
|
+
// Scoped slot effects — reactive resolution of {{propName}} in consumer templates
|
|
160
541
|
for (const s of slots) {
|
|
161
542
|
if (s.name && s.slotProps.length > 0) {
|
|
162
|
-
const propsObj = s.slotProps.map(sp =>
|
|
543
|
+
const propsObj = s.slotProps.map(sp => {
|
|
544
|
+
const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
|
|
545
|
+
return `${sp.prop}: ${ref}`;
|
|
546
|
+
}).join(', ');
|
|
163
547
|
lines.push(` if (this.__slotTpl_${s.name}) {`);
|
|
164
548
|
lines.push(' __effect(() => {');
|
|
165
549
|
lines.push(` const __props = { ${propsObj} };`);
|
|
@@ -173,152 +557,484 @@ export function generateComponent(parseResult) {
|
|
|
173
557
|
}
|
|
174
558
|
}
|
|
175
559
|
|
|
176
|
-
//
|
|
177
|
-
for (const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
560
|
+
// Child component reactive prop bindings
|
|
561
|
+
for (const cc of childComponents) {
|
|
562
|
+
for (const pb of cc.propBindings) {
|
|
563
|
+
let ref;
|
|
564
|
+
if (pb.type === 'prop') {
|
|
565
|
+
ref = `this._s_${pb.expr}()`;
|
|
566
|
+
} else if (pb.type === 'computed') {
|
|
567
|
+
ref = `this._c_${pb.expr}()`;
|
|
568
|
+
} else if (pb.type === 'signal') {
|
|
569
|
+
ref = `this._${pb.expr}()`;
|
|
570
|
+
} else if (pb.type === 'constant') {
|
|
571
|
+
ref = `this._const_${pb.expr}`;
|
|
572
|
+
} else {
|
|
573
|
+
ref = `this._${pb.expr}()`;
|
|
574
|
+
}
|
|
575
|
+
lines.push(' __effect(() => {');
|
|
576
|
+
lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
|
|
577
|
+
lines.push(' });');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// User effects
|
|
582
|
+
for (const eff of effects) {
|
|
583
|
+
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
181
584
|
lines.push(' __effect(() => {');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
585
|
+
// Indent each line of the body
|
|
586
|
+
const bodyLines = body.split('\n');
|
|
587
|
+
for (const line of bodyLines) {
|
|
588
|
+
lines.push(` ${line}`);
|
|
589
|
+
}
|
|
188
590
|
lines.push(' });');
|
|
189
591
|
}
|
|
190
592
|
|
|
191
|
-
// Event listeners
|
|
593
|
+
// Event listeners
|
|
192
594
|
for (const e of events) {
|
|
193
595
|
lines.push(` this.${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
194
596
|
}
|
|
195
597
|
|
|
196
|
-
|
|
197
|
-
|
|
598
|
+
// Show effects — one __effect per ShowBinding
|
|
599
|
+
for (const sb of showBindings) {
|
|
600
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
601
|
+
lines.push(' __effect(() => {');
|
|
602
|
+
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
603
|
+
lines.push(' });');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Model effects — signal → DOM (one __effect per ModelBinding)
|
|
607
|
+
for (const mb of modelBindings) {
|
|
608
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
609
|
+
// Radio: compare signal value to radioValue
|
|
610
|
+
lines.push(' __effect(() => {');
|
|
611
|
+
lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
612
|
+
lines.push(' });');
|
|
613
|
+
} else if (mb.prop === 'checked') {
|
|
614
|
+
// Checkbox: coerce to boolean
|
|
615
|
+
lines.push(' __effect(() => {');
|
|
616
|
+
lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
617
|
+
lines.push(' });');
|
|
618
|
+
} else {
|
|
619
|
+
// Value-based (text, number, textarea, select): nullish coalesce to ''
|
|
620
|
+
lines.push(' __effect(() => {');
|
|
621
|
+
lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
622
|
+
lines.push(' });');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Model event listeners — DOM → signal (one addEventListener per ModelBinding)
|
|
627
|
+
for (const mb of modelBindings) {
|
|
628
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
629
|
+
// Checkbox: read e.target.checked
|
|
630
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
631
|
+
} else if (mb.coerce) {
|
|
632
|
+
// Number input: wrap in Number()
|
|
633
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
634
|
+
} else {
|
|
635
|
+
// All others: read e.target.value
|
|
636
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
198
639
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
640
|
+
// Attr binding effects — one __effect per AttrBinding
|
|
641
|
+
for (const ab of attrBindings) {
|
|
642
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
643
|
+
if (ab.kind === 'attr') {
|
|
644
|
+
lines.push(' __effect(() => {');
|
|
645
|
+
lines.push(` const __v = ${expr};`);
|
|
646
|
+
lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
|
|
647
|
+
lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
648
|
+
lines.push(' });');
|
|
649
|
+
} else if (ab.kind === 'bool') {
|
|
650
|
+
lines.push(' __effect(() => {');
|
|
651
|
+
lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
|
|
652
|
+
lines.push(' });');
|
|
653
|
+
} else if (ab.kind === 'class') {
|
|
654
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
655
|
+
// Object expression: iterate entries, classList.add/remove
|
|
656
|
+
lines.push(' __effect(() => {');
|
|
657
|
+
lines.push(` const __obj = ${expr};`);
|
|
658
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
659
|
+
lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
|
|
660
|
+
lines.push(' }');
|
|
661
|
+
lines.push(' });');
|
|
662
|
+
} else {
|
|
663
|
+
// String expression: set className
|
|
664
|
+
lines.push(' __effect(() => {');
|
|
665
|
+
lines.push(` this.${ab.varName}.className = ${expr};`);
|
|
666
|
+
lines.push(' });');
|
|
667
|
+
}
|
|
668
|
+
} else if (ab.kind === 'style') {
|
|
669
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
670
|
+
// Object expression: iterate entries, set style[key]
|
|
671
|
+
lines.push(' __effect(() => {');
|
|
672
|
+
lines.push(` const __obj = ${expr};`);
|
|
673
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
674
|
+
lines.push(` this.${ab.varName}.style[__k] = __val;`);
|
|
675
|
+
lines.push(' }');
|
|
676
|
+
lines.push(' });');
|
|
677
|
+
} else {
|
|
678
|
+
// String expression: set cssText
|
|
679
|
+
lines.push(' __effect(() => {');
|
|
680
|
+
lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
|
|
681
|
+
lines.push(' });');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
203
684
|
}
|
|
685
|
+
|
|
686
|
+
// ── if effects ──
|
|
687
|
+
for (const ifBlock of ifBlocks) {
|
|
688
|
+
const vn = ifBlock.varName;
|
|
689
|
+
lines.push(' __effect(() => {');
|
|
690
|
+
lines.push(' let __branch = null;');
|
|
691
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
692
|
+
const branch = ifBlock.branches[i];
|
|
693
|
+
if (branch.type === 'if') {
|
|
694
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
695
|
+
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
696
|
+
} else if (branch.type === 'else-if') {
|
|
697
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
698
|
+
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
699
|
+
} else {
|
|
700
|
+
// else
|
|
701
|
+
lines.push(` else { __branch = ${i}; }`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
lines.push(` if (__branch === this.${vn}_active) return;`);
|
|
705
|
+
// Remove previous branch
|
|
706
|
+
lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
|
|
707
|
+
// Insert new branch
|
|
708
|
+
lines.push(' if (__branch !== null) {');
|
|
709
|
+
const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
|
|
710
|
+
lines.push(` const tpl = [${tplArray}][__branch];`);
|
|
711
|
+
lines.push(' const clone = tpl.content.cloneNode(true);');
|
|
712
|
+
lines.push(' const node = clone.firstChild;');
|
|
713
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
714
|
+
lines.push(` this.${vn}_current = node;`);
|
|
715
|
+
// Setup bindings/events for active branch (only if any branch has bindings/events)
|
|
716
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
717
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
718
|
+
(b.events && b.events.length > 0) ||
|
|
719
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
720
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
721
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
722
|
+
);
|
|
723
|
+
if (hasSetup) {
|
|
724
|
+
lines.push(` this.${vn}_setup(node, __branch);`);
|
|
725
|
+
}
|
|
726
|
+
lines.push(' }');
|
|
727
|
+
lines.push(` this.${vn}_active = __branch;`);
|
|
728
|
+
lines.push(' });');
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ── each effects ──
|
|
732
|
+
for (const forBlock of forBlocks) {
|
|
733
|
+
const vn = forBlock.varName;
|
|
734
|
+
const { itemVar, indexVar, source } = forBlock;
|
|
735
|
+
|
|
736
|
+
const signalNamesSet = new Set(signalNames);
|
|
737
|
+
const computedNamesSet = new Set(computedNames);
|
|
738
|
+
|
|
739
|
+
// Transform the source expression
|
|
740
|
+
const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
741
|
+
|
|
742
|
+
lines.push(' __effect(() => {');
|
|
743
|
+
lines.push(` const __source = ${sourceExpr};`);
|
|
744
|
+
lines.push('');
|
|
745
|
+
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
746
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
747
|
+
lines.push('');
|
|
748
|
+
lines.push(" const __iter = typeof __source === 'number'");
|
|
749
|
+
lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
|
|
750
|
+
lines.push(' : (__source || []);');
|
|
751
|
+
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
|
+
|
|
769
|
+
// Setup events per item
|
|
770
|
+
for (const e of forBlock.events) {
|
|
771
|
+
const nodeRef = pathExpr(e.path, 'node');
|
|
772
|
+
lines.push(` ${nodeRef}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Setup show per item
|
|
776
|
+
for (const sb of forBlock.showBindings) {
|
|
777
|
+
const nodeRef = pathExpr(sb.path, 'node');
|
|
778
|
+
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
779
|
+
const expr = sb.expression;
|
|
780
|
+
lines.push(` ${nodeRef}.style.display = (${expr}) ? '' : 'none';`);
|
|
781
|
+
} else {
|
|
782
|
+
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
783
|
+
lines.push(` __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Setup attr bindings per item
|
|
788
|
+
for (const ab of forBlock.attrBindings) {
|
|
789
|
+
const nodeRef = pathExpr(ab.path, 'node');
|
|
790
|
+
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
791
|
+
const expr = ab.expression;
|
|
792
|
+
lines.push(` const __val_${ab.varName} = ${expr};`);
|
|
793
|
+
lines.push(` if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
794
|
+
} else {
|
|
795
|
+
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
796
|
+
lines.push(` __effect(() => {`);
|
|
797
|
+
lines.push(` const __val = ${expr};`);
|
|
798
|
+
lines.push(` if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
799
|
+
lines.push(` else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
800
|
+
lines.push(` });`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Setup model bindings per item
|
|
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
|
+
}
|
|
826
|
+
|
|
827
|
+
// Setup scoped slot resolution per item
|
|
828
|
+
for (const s of (forBlock.slots || [])) {
|
|
829
|
+
if (s.slotProps.length > 0) {
|
|
830
|
+
const slotNodeRef = pathExpr(s.path, 'node');
|
|
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
|
+
}
|
|
841
|
+
}
|
|
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
|
+
}
|
|
848
|
+
|
|
849
|
+
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
850
|
+
for (const hook of onMountHooks) {
|
|
851
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
852
|
+
const bodyLines = body.split('\n');
|
|
853
|
+
for (const line of bodyLines) {
|
|
854
|
+
lines.push(` ${line}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
204
858
|
lines.push(' }');
|
|
205
859
|
lines.push('');
|
|
206
860
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
lines.push(
|
|
210
|
-
|
|
861
|
+
// disconnectedCallback (only when destroy hooks exist)
|
|
862
|
+
if (onDestroyHooks.length > 0) {
|
|
863
|
+
lines.push(' disconnectedCallback() {');
|
|
864
|
+
for (const hook of onDestroyHooks) {
|
|
865
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
866
|
+
const bodyLines = body.split('\n');
|
|
867
|
+
for (const line of bodyLines) {
|
|
868
|
+
lines.push(` ${line}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
lines.push(' }');
|
|
211
872
|
lines.push('');
|
|
212
873
|
}
|
|
213
874
|
|
|
214
|
-
//
|
|
215
|
-
if (
|
|
875
|
+
// attributeChangedCallback (if props exist)
|
|
876
|
+
if (propDefs.length > 0) {
|
|
877
|
+
lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
|
|
878
|
+
for (const p of propDefs) {
|
|
879
|
+
const defaultVal = p.default;
|
|
880
|
+
let updateExpr;
|
|
881
|
+
|
|
882
|
+
if (defaultVal === 'true' || defaultVal === 'false') {
|
|
883
|
+
// Boolean coercion: attribute presence = true
|
|
884
|
+
updateExpr = `this._s_${p.name}(newVal != null)`;
|
|
885
|
+
} else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
|
|
886
|
+
// Number coercion
|
|
887
|
+
updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
|
|
888
|
+
} else if (defaultVal === 'undefined') {
|
|
889
|
+
// Undefined default — pass through
|
|
890
|
+
updateExpr = `this._s_${p.name}(newVal)`;
|
|
891
|
+
} else {
|
|
892
|
+
// String default — use nullish coalescing
|
|
893
|
+
updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
|
|
897
|
+
}
|
|
898
|
+
lines.push(' }');
|
|
899
|
+
lines.push('');
|
|
900
|
+
|
|
901
|
+
// Public getters and setters
|
|
902
|
+
for (const p of propDefs) {
|
|
903
|
+
lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
|
|
904
|
+
lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
|
|
905
|
+
lines.push('');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// _emit method (if emits declared)
|
|
910
|
+
if (emits.length > 0) {
|
|
216
911
|
lines.push(' _emit(name, detail) {');
|
|
217
912
|
lines.push(' this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));');
|
|
218
913
|
lines.push(' }');
|
|
219
914
|
lines.push('');
|
|
220
915
|
}
|
|
221
916
|
|
|
222
|
-
// User methods (
|
|
917
|
+
// User methods (prefixed with _)
|
|
223
918
|
for (const m of methods) {
|
|
224
|
-
|
|
919
|
+
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames);
|
|
225
920
|
lines.push(` _${m.name}(${m.params}) {`);
|
|
226
|
-
|
|
921
|
+
const bodyLines = body.split('\n');
|
|
922
|
+
for (const line of bodyLines) {
|
|
923
|
+
lines.push(` ${line}`);
|
|
924
|
+
}
|
|
227
925
|
lines.push(' }');
|
|
228
926
|
lines.push('');
|
|
229
927
|
}
|
|
230
928
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
929
|
+
// ── Ref getter properties ──
|
|
930
|
+
for (const rd of refs) {
|
|
931
|
+
// Find matching RefBinding
|
|
932
|
+
const rb = refBindings.find(b => b.refName === rd.refName);
|
|
933
|
+
if (rb) {
|
|
934
|
+
lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
|
|
935
|
+
lines.push('');
|
|
936
|
+
}
|
|
937
|
+
}
|
|
237
938
|
|
|
238
|
-
|
|
239
|
-
|
|
939
|
+
// ── if setup methods ──
|
|
940
|
+
for (const ifBlock of ifBlocks) {
|
|
941
|
+
const vn = ifBlock.varName;
|
|
942
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
943
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
944
|
+
(b.events && b.events.length > 0) ||
|
|
945
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
946
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
947
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
948
|
+
);
|
|
949
|
+
if (!hasSetup) continue;
|
|
240
950
|
|
|
241
|
-
|
|
951
|
+
lines.push(` ${vn}_setup(node, branch) {`);
|
|
952
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
953
|
+
const branch = ifBlock.branches[i];
|
|
954
|
+
const hasBranchSetup =
|
|
955
|
+
(branch.bindings && branch.bindings.length > 0) ||
|
|
956
|
+
(branch.events && branch.events.length > 0) ||
|
|
957
|
+
(branch.showBindings && branch.showBindings.length > 0) ||
|
|
958
|
+
(branch.attrBindings && branch.attrBindings.length > 0) ||
|
|
959
|
+
(branch.modelBindings && branch.modelBindings.length > 0);
|
|
960
|
+
if (!hasBranchSetup) continue;
|
|
242
961
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
* Props → this._s_name(), reactive vars → this._name(), computeds → this._c_name()
|
|
246
|
-
*/
|
|
247
|
-
function transformExpr(expr, propsSet, rootVarNames, computedNames) {
|
|
248
|
-
let r = expr;
|
|
249
|
-
for (const p of propsSet) {
|
|
250
|
-
r = r.replace(new RegExp(`\\b${p}\\b`, 'g'), `this._s_${p}()`);
|
|
251
|
-
}
|
|
252
|
-
for (const n of rootVarNames) {
|
|
253
|
-
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._${n}()`);
|
|
254
|
-
}
|
|
255
|
-
for (const n of computedNames) {
|
|
256
|
-
r = r.replace(new RegExp(`\\b${n}\\b`, 'g'), `this._c_${n}()`);
|
|
257
|
-
}
|
|
258
|
-
return r;
|
|
259
|
-
}
|
|
962
|
+
const keyword = i === 0 ? 'if' : 'else if';
|
|
963
|
+
lines.push(` ${keyword} (branch === ${i}) {`);
|
|
260
964
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
965
|
+
// Bindings: generate DOM refs and effects for text bindings
|
|
966
|
+
for (const b of branch.bindings) {
|
|
967
|
+
lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
|
|
968
|
+
if (b.type === 'prop') {
|
|
969
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
|
|
970
|
+
} else if (b.type === 'signal') {
|
|
971
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
|
|
972
|
+
} else if (b.type === 'computed') {
|
|
973
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
|
|
974
|
+
} else {
|
|
975
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._${b.name}() ?? ''; });`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
266
978
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
979
|
+
// Events: generate addEventListener
|
|
980
|
+
for (const e of branch.events) {
|
|
981
|
+
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
982
|
+
lines.push(` ${e.varName}.addEventListener('${e.event}', this._${e.handler}.bind(this));`);
|
|
983
|
+
}
|
|
272
984
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
for (const n of computedNames) {
|
|
281
|
-
r = r.replace(new RegExp(`(?<!this\\._c_)(?<!\\.)\\b${n}\\b`, 'g'), `this._c_${n}()`);
|
|
282
|
-
}
|
|
985
|
+
// Show bindings: generate effects
|
|
986
|
+
for (const sb of branch.showBindings) {
|
|
987
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
988
|
+
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
989
|
+
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
990
|
+
}
|
|
283
991
|
|
|
284
|
-
|
|
285
|
-
|
|
992
|
+
// Attr bindings: generate effects
|
|
993
|
+
for (const ab of branch.attrBindings) {
|
|
994
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames);
|
|
995
|
+
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
996
|
+
lines.push(` __effect(() => {`);
|
|
997
|
+
lines.push(` const __val = ${expr};`);
|
|
998
|
+
lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
999
|
+
lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
|
|
1000
|
+
lines.push(` });`);
|
|
1001
|
+
}
|
|
286
1002
|
|
|
287
|
-
|
|
288
|
-
|
|
1003
|
+
// Model bindings: generate effects and listeners
|
|
1004
|
+
for (const mb of (branch.modelBindings || [])) {
|
|
1005
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
1006
|
+
lines.push(` const ${mb.varName} = ${nodeRef};`);
|
|
1007
|
+
// Effect (signal → DOM)
|
|
1008
|
+
lines.push(` __effect(() => {`);
|
|
1009
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
1010
|
+
lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
1011
|
+
} else if (mb.prop === 'checked') {
|
|
1012
|
+
lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
1013
|
+
} else {
|
|
1014
|
+
lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
1015
|
+
}
|
|
1016
|
+
lines.push(` });`);
|
|
1017
|
+
// Listener (DOM → signal)
|
|
1018
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
1019
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
1020
|
+
} else if (mb.coerce) {
|
|
1021
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
1022
|
+
} else {
|
|
1023
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
289
1026
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (b.type === 'computed') return `this._c_${b.name}()`;
|
|
296
|
-
return `this._${b.name}()`;
|
|
297
|
-
}
|
|
1027
|
+
lines.push(' }');
|
|
1028
|
+
}
|
|
1029
|
+
lines.push(' }');
|
|
1030
|
+
lines.push('');
|
|
1031
|
+
}
|
|
298
1032
|
|
|
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
|
-
}
|
|
1033
|
+
lines.push('}');
|
|
1034
|
+
lines.push('');
|
|
308
1035
|
|
|
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
|
-
}
|
|
1036
|
+
// ── 5. Custom element registration ──
|
|
1037
|
+
lines.push(`customElements.define('${tagName}', ${className});`);
|
|
318
1038
|
|
|
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));
|
|
1039
|
+
return lines.join('\n');
|
|
324
1040
|
}
|