@sprlab/wccompiler 0.13.0 → 0.15.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 +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2078 -2074
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -479
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -114
- package/lib/tree-walker.js +1013 -1013
- package/lib/types.js +262 -262
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/lib/codegen.js
CHANGED
|
@@ -1,2074 +1,2078 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Generator for wcCompiler v2.
|
|
3
|
-
*
|
|
4
|
-
* Takes a complete ParseResult (with bindings, events populated by tree-walker)
|
|
5
|
-
* and produces a self-contained JavaScript string with:
|
|
6
|
-
* - Inline mini reactive runtime (zero imports)
|
|
7
|
-
* - Scoped CSS injection
|
|
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.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { reactiveRuntime, buildInlineRuntime } from './reactive-runtime.js';
|
|
17
|
-
import { scopeCSS } from './css-scoper.js';
|
|
18
|
-
import { camelToKebab } from './parser-extractors.js';
|
|
19
|
-
|
|
20
|
-
/** @import { ParseResult } from './types.js' */
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Convert a path array to a JS expression string.
|
|
24
|
-
* e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
|
|
25
|
-
*
|
|
26
|
-
* @param {string[]} parts
|
|
27
|
-
* @param {string} rootVar
|
|
28
|
-
* @returns {string}
|
|
29
|
-
*/
|
|
30
|
-
export function pathExpr(parts, rootVar) {
|
|
31
|
-
return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Escape special regex characters in a string.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} str
|
|
38
|
-
* @returns {string}
|
|
39
|
-
*/
|
|
40
|
-
function escapeRegex(str) {
|
|
41
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get the signal reference for a slot prop source expression.
|
|
46
|
-
*
|
|
47
|
-
* @param {string} source — Source variable name from :prop="source"
|
|
48
|
-
* @param {string[]} signalNames — Signal variable names
|
|
49
|
-
* @param {string[]} computedNames — Computed variable names
|
|
50
|
-
* @param {Set<string>} propNames — Prop names from defineProps
|
|
51
|
-
* @returns {string}
|
|
52
|
-
*/
|
|
53
|
-
function slotPropRef(source, signalNames, computedNames, propNames) {
|
|
54
|
-
if (propNames.has(source)) return `this._s_${source}()`;
|
|
55
|
-
if (computedNames.includes(source)) return `this._c_${source}()`;
|
|
56
|
-
if (signalNames.includes(source)) return `this._${source}()`;
|
|
57
|
-
return `'${source}'`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Transform an expression by rewriting signal/computed variable references
|
|
62
|
-
* to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
|
|
63
|
-
*
|
|
64
|
-
* Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
|
|
65
|
-
* Also handles `emitsObjectName(` → `this._emit(` transformation.
|
|
66
|
-
*
|
|
67
|
-
* Uses word-boundary regex for each known signal/computed name.
|
|
68
|
-
* Does NOT transform if the name is followed by `.set(` (that's a write,
|
|
69
|
-
* handled by transformMethodBody).
|
|
70
|
-
*
|
|
71
|
-
* @param {string} expr — Expression to transform
|
|
72
|
-
* @param {string[]} signalNames — Signal variable names
|
|
73
|
-
* @param {string[]} computedNames — Computed variable names
|
|
74
|
-
* @param {string|null} [propsObjectName] — Props object variable name
|
|
75
|
-
* @param {Set<string>} [propNames] — Set of prop names
|
|
76
|
-
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
77
|
-
* @returns {string}
|
|
78
|
-
*/
|
|
79
|
-
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = [], modelVarMap = new Map()) {
|
|
80
|
-
let result = expr;
|
|
81
|
-
|
|
82
|
-
// Transform emit calls: emitsObjectName( → this._emit(
|
|
83
|
-
if (emitsObjectName) {
|
|
84
|
-
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
85
|
-
result = result.replace(emitsRe, 'this._emit(');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Transform method calls: methodName( → this._methodName(
|
|
89
|
-
for (const name of methodNames) {
|
|
90
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
91
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
92
|
-
const methodRe = new RegExp(`\\b${name}\\(`, 'g');
|
|
93
|
-
result = result.replace(methodRe, `this._${name}(`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Transform props.x → this._s_x() BEFORE signal/computed transforms
|
|
97
|
-
if (propsObjectName && propNames.size > 0) {
|
|
98
|
-
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
99
|
-
result = result.replace(propsRe, (match, propName) => {
|
|
100
|
-
if (propNames.has(propName)) {
|
|
101
|
-
return `this._s_${propName}()`;
|
|
102
|
-
}
|
|
103
|
-
return match; // leave unknown props unchanged
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
|
|
108
|
-
for (const propName of propNames) {
|
|
109
|
-
if (propsObjectName && propName === propsObjectName) continue;
|
|
110
|
-
if (emitsObjectName && propName === emitsObjectName) continue;
|
|
111
|
-
const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
112
|
-
result = result.replace(bareRe, `this._s_${propName}()`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Transform model signal reads: varName() → this._m_{propName}() (BEFORE regular signals)
|
|
116
|
-
for (const [varName, propNameVal] of modelVarMap) {
|
|
117
|
-
if (propsObjectName && varName === propsObjectName) continue;
|
|
118
|
-
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
119
|
-
// First: transform varName() calls → this._m_propName()
|
|
120
|
-
const callRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
121
|
-
result = result.replace(callRe, `this._m_${propNameVal}()`);
|
|
122
|
-
// Then: transform bare varName references (not followed by ( or .set()) → this._m_propName()
|
|
123
|
-
const bareRe = new RegExp(`\\b(${varName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
124
|
-
result = result.replace(bareRe, `this._m_${propNameVal}()`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Transform computed names first (to avoid partial matches with signals)
|
|
128
|
-
for (const name of computedNames) {
|
|
129
|
-
// Skip propsObjectName and emitsObjectName
|
|
130
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
131
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
132
|
-
// First: transform name() calls → this._c_name() (replace the call, not just the name)
|
|
133
|
-
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
134
|
-
result = result.replace(callRe, `this._c_${name}()`);
|
|
135
|
-
// Then: transform bare name references (not followed by ( or .set() → this._c_name()
|
|
136
|
-
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
137
|
-
result = result.replace(bareRe, `this._c_${name}()`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Transform signal names
|
|
141
|
-
for (const name of signalNames) {
|
|
142
|
-
// Skip propsObjectName and emitsObjectName
|
|
143
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
144
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
145
|
-
// First: transform name() calls → this._name() (replace the call, not just the name)
|
|
146
|
-
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
147
|
-
result = result.replace(callRe, `this._${name}()`);
|
|
148
|
-
// Then: transform bare name references (not followed by ( or .set() → this._name()
|
|
149
|
-
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
150
|
-
result = result.replace(bareRe, `this._${name}()`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Transform constant names → this._const_name (no function call)
|
|
154
|
-
for (const name of constantNames) {
|
|
155
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
156
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
157
|
-
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
158
|
-
result = result.replace(bareRe, `this._const_${name}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Transform a method/effect body by rewriting signal writes and reads.
|
|
166
|
-
*
|
|
167
|
-
* - `emitsObjectName(` → `this._emit(` (emit call)
|
|
168
|
-
* - `props.x` → `this._s_x()` (prop access)
|
|
169
|
-
* - `varName.set(value)` → `this._modelSet_{propName}(value)` (model signal write)
|
|
170
|
-
* - `x.set(value)` → `this._x(value)` (signal write via setter)
|
|
171
|
-
* - `varName()` → `this._m_{propName}()` (model signal read)
|
|
172
|
-
* - `x()` → `this._x()` (signal read)
|
|
173
|
-
* - Computed `x()` → `this._c_x()` (computed read)
|
|
174
|
-
*
|
|
175
|
-
* @param {string} body — Function body to transform
|
|
176
|
-
* @param {string[]} signalNames — Signal variable names
|
|
177
|
-
* @param {string[]} computedNames — Computed variable names
|
|
178
|
-
* @param {string|null} [propsObjectName] — Props object variable name
|
|
179
|
-
* @param {Set<string>} [propNames] — Set of prop names
|
|
180
|
-
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
181
|
-
* @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
|
|
182
|
-
* @param {string[]} [constantNames] — Constant variable names
|
|
183
|
-
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
184
|
-
* @returns {string}
|
|
185
|
-
*/
|
|
186
|
-
export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = [], modelVarMap = new Map()) {
|
|
187
|
-
let result = body;
|
|
188
|
-
|
|
189
|
-
// 0a. Transform emit calls: emitsObjectName( → this._emit(
|
|
190
|
-
if (emitsObjectName) {
|
|
191
|
-
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
192
|
-
result = result.replace(emitsRe, 'this._emit(');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 0b. Transform props.x → this._s_x() BEFORE other transforms
|
|
196
|
-
if (propsObjectName && propNames.size > 0) {
|
|
197
|
-
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
198
|
-
result = result.replace(propsRe, (match, propName) => {
|
|
199
|
-
if (propNames.has(propName)) {
|
|
200
|
-
return `this._s_${propName}()`;
|
|
201
|
-
}
|
|
202
|
-
return match;
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 0c. Transform ref access: varName.value → this._varName.value
|
|
207
|
-
for (const name of refVarNames) {
|
|
208
|
-
const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
|
|
209
|
-
result = result.replace(refRe, `this._${name}.value`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 0d. Transform model signal writes: varName.set(expr) → this._modelSet_{propName}(expr)
|
|
213
|
-
// Must run BEFORE regular signal .set() transforms
|
|
214
|
-
for (const [varName, propNameVal] of modelVarMap) {
|
|
215
|
-
if (propsObjectName && varName === propsObjectName) continue;
|
|
216
|
-
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
217
|
-
const setRe = new RegExp(`\\b${varName}\\.set\\(`, 'g');
|
|
218
|
-
result = result.replace(setRe, `this._modelSet_${propNameVal}(`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// 1. Transform signal writes: x.set(value) → this._x(value)
|
|
222
|
-
for (const name of signalNames) {
|
|
223
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
224
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
225
|
-
const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
|
|
226
|
-
result = result.replace(setRe, `this._${name}(`);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 1b. Transform model signal reads: varName() → this._m_{propName}()
|
|
230
|
-
// Must run BEFORE regular signal read transforms
|
|
231
|
-
for (const [varName, propNameVal] of modelVarMap) {
|
|
232
|
-
if (propsObjectName && varName === propsObjectName) continue;
|
|
233
|
-
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
234
|
-
const readRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
235
|
-
result = result.replace(readRe, `this._m_${propNameVal}()`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// 2. Transform computed reads: x() → this._c_x()
|
|
239
|
-
for (const name of computedNames) {
|
|
240
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
241
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
242
|
-
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
243
|
-
result = result.replace(readRe, `this._c_${name}()`);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// 3. Transform signal reads: x() → this._x()
|
|
247
|
-
for (const name of signalNames) {
|
|
248
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
249
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
250
|
-
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
251
|
-
result = result.replace(readRe, `this._${name}()`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 4. Transform constant reads: name → this._const_name
|
|
255
|
-
for (const name of constantNames) {
|
|
256
|
-
if (propsObjectName && name === propsObjectName) continue;
|
|
257
|
-
if (emitsObjectName && name === emitsObjectName) continue;
|
|
258
|
-
const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
|
|
259
|
-
result = result.replace(bareRe, `this._const_${name}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Transform an expression within the scope of an each block.
|
|
267
|
-
* - References to itemVar and indexVar are left UNTRANSFORMED
|
|
268
|
-
* - References to component variables (props, reactive, computed) ARE transformed
|
|
269
|
-
*
|
|
270
|
-
* @param {string} expr - The expression to transform
|
|
271
|
-
* @param {string} itemVar - Name of the iteration variable
|
|
272
|
-
* @param {string | null} indexVar - Name of the index variable
|
|
273
|
-
* @param {Set<string>} propsSet
|
|
274
|
-
* @param {Set<string>} rootVarNames - Set of signal names
|
|
275
|
-
* @param {Set<string>} computedNames
|
|
276
|
-
* @returns {string}
|
|
277
|
-
*/
|
|
278
|
-
export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
279
|
-
let r = expr;
|
|
280
|
-
const excludeSet = new Set([itemVar]);
|
|
281
|
-
if (indexVar) excludeSet.add(indexVar);
|
|
282
|
-
|
|
283
|
-
for (const p of propsSet) {
|
|
284
|
-
if (excludeSet.has(p)) continue;
|
|
285
|
-
// First: transform name() calls → this._s_name() (don't double-call)
|
|
286
|
-
r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
|
|
287
|
-
// Then: transform bare name references
|
|
288
|
-
r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
|
|
289
|
-
}
|
|
290
|
-
for (const n of rootVarNames) {
|
|
291
|
-
if (excludeSet.has(n)) continue;
|
|
292
|
-
// First: transform name() calls → this._name() (don't double-call)
|
|
293
|
-
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
|
|
294
|
-
// Then: transform bare name references
|
|
295
|
-
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
|
|
296
|
-
}
|
|
297
|
-
for (const n of computedNames) {
|
|
298
|
-
if (excludeSet.has(n)) continue;
|
|
299
|
-
// First: transform name() calls → this._c_name() (don't double-call)
|
|
300
|
-
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
|
|
301
|
-
// Then: transform bare name references
|
|
302
|
-
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
|
|
303
|
-
}
|
|
304
|
-
return r;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Check if a binding name is static within an each scope (references only item/index).
|
|
309
|
-
* A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
|
|
310
|
-
*
|
|
311
|
-
* @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
|
|
312
|
-
* @param {string} itemVar
|
|
313
|
-
* @param {string | null} indexVar
|
|
314
|
-
* @returns {boolean}
|
|
315
|
-
*/
|
|
316
|
-
export function isStaticForBinding(name, itemVar, indexVar) {
|
|
317
|
-
if (name === itemVar || name.startsWith(itemVar + '.')) return true;
|
|
318
|
-
if (indexVar && name === indexVar) return true;
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Check if an expression is static within an each scope (references only item/index, no component vars).
|
|
324
|
-
*
|
|
325
|
-
* @param {string} expr
|
|
326
|
-
* @param {string} itemVar
|
|
327
|
-
* @param {string | null} indexVar
|
|
328
|
-
* @param {Set<string>} propsSet
|
|
329
|
-
* @param {Set<string>} rootVarNames
|
|
330
|
-
* @param {Set<string>} computedNames
|
|
331
|
-
* @returns {boolean}
|
|
332
|
-
*/
|
|
333
|
-
export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
334
|
-
const excludeSet = new Set([itemVar]);
|
|
335
|
-
if (indexVar) excludeSet.add(indexVar);
|
|
336
|
-
|
|
337
|
-
for (const p of propsSet) {
|
|
338
|
-
if (excludeSet.has(p)) continue;
|
|
339
|
-
if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
|
|
340
|
-
}
|
|
341
|
-
for (const n of rootVarNames) {
|
|
342
|
-
if (excludeSet.has(n)) continue;
|
|
343
|
-
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
344
|
-
}
|
|
345
|
-
for (const n of computedNames) {
|
|
346
|
-
if (excludeSet.has(n)) continue;
|
|
347
|
-
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
348
|
-
}
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Generate the JS expression for an event handler based on its type:
|
|
354
|
-
* - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
|
|
355
|
-
* - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
|
|
356
|
-
* - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
|
|
357
|
-
*
|
|
358
|
-
* @param {string} handler — The raw handler string from the template
|
|
359
|
-
* @param {string[]} signalNames
|
|
360
|
-
* @param {string[]} computedNames
|
|
361
|
-
* @param {string|null} propsObjectName
|
|
362
|
-
* @param {Set<string>} propNames
|
|
363
|
-
* @param {string|null} emitsObjectName
|
|
364
|
-
* @param {string[]} constantNames
|
|
365
|
-
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
366
|
-
* @returns {string}
|
|
367
|
-
*/
|
|
368
|
-
export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap = new Map()) {
|
|
369
|
-
if (handler.includes('=>')) {
|
|
370
|
-
// Arrow function expression: (e) => removeItem(item)
|
|
371
|
-
const arrowIdx = handler.indexOf('=>');
|
|
372
|
-
const params = handler.slice(0, arrowIdx).trim();
|
|
373
|
-
let body = handler.slice(arrowIdx + 2).trim();
|
|
374
|
-
body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames, modelVarMap);
|
|
375
|
-
return `${params} => { ${body}; }`;
|
|
376
|
-
} else if (handler.includes('(')) {
|
|
377
|
-
// Function call expression: removeItem(item)
|
|
378
|
-
const parenIdx = handler.indexOf('(');
|
|
379
|
-
const fnName = handler.slice(0, parenIdx).trim();
|
|
380
|
-
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
381
|
-
const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap) : '';
|
|
382
|
-
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
383
|
-
} else {
|
|
384
|
-
// Simple method name
|
|
385
|
-
return `this._${handler}.bind(this)`;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Generate the JS expression for an event handler inside an each block.
|
|
391
|
-
* Similar to generateEventHandler but uses transformForExpr for the each scope.
|
|
392
|
-
*
|
|
393
|
-
* @param {string} handler
|
|
394
|
-
* @param {string} itemVar
|
|
395
|
-
* @param {string|null} indexVar
|
|
396
|
-
* @param {Set<string>} propNames
|
|
397
|
-
* @param {Set<string>} signalNamesSet
|
|
398
|
-
* @param {Set<string>} computedNamesSet
|
|
399
|
-
* @returns {string}
|
|
400
|
-
*/
|
|
401
|
-
export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
402
|
-
if (handler.includes('=>')) {
|
|
403
|
-
// Arrow function expression
|
|
404
|
-
const arrowIdx = handler.indexOf('=>');
|
|
405
|
-
const params = handler.slice(0, arrowIdx).trim();
|
|
406
|
-
let body = handler.slice(arrowIdx + 2).trim();
|
|
407
|
-
body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
408
|
-
return `${params} => { ${body}; }`;
|
|
409
|
-
} else if (handler.includes('(')) {
|
|
410
|
-
// Function call expression: removeItem(item)
|
|
411
|
-
const parenIdx = handler.indexOf('(');
|
|
412
|
-
const fnName = handler.slice(0, parenIdx).trim();
|
|
413
|
-
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
414
|
-
const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
|
|
415
|
-
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
416
|
-
} else {
|
|
417
|
-
// Simple method name
|
|
418
|
-
return `this._${handler}.bind(this)`;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Generate per-item setup code for bindings, events, show, attr, model, and slots.
|
|
424
|
-
* Used by both keyed and non-keyed each effects.
|
|
425
|
-
*
|
|
426
|
-
* @param {string[]} lines — Output lines array
|
|
427
|
-
* @param {object} forBlock — ForBlock with bindings, events, etc.
|
|
428
|
-
* @param {string} itemVar
|
|
429
|
-
* @param {string|null} indexVar
|
|
430
|
-
* @param {Set<string>} propNames
|
|
431
|
-
* @param {Set<string>} signalNamesSet
|
|
432
|
-
* @param {Set<string>} computedNamesSet
|
|
433
|
-
*/
|
|
434
|
-
function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
435
|
-
const indent = ' ';
|
|
436
|
-
|
|
437
|
-
// Bindings
|
|
438
|
-
for (const b of forBlock.bindings) {
|
|
439
|
-
const nodeRef = pathExpr(b.path, 'node');
|
|
440
|
-
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
441
|
-
lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
442
|
-
} else {
|
|
443
|
-
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
444
|
-
lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Events
|
|
449
|
-
for (const e of forBlock.events) {
|
|
450
|
-
const nodeRef = pathExpr(e.path, 'node');
|
|
451
|
-
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
452
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Show
|
|
456
|
-
for (const sb of forBlock.showBindings) {
|
|
457
|
-
const nodeRef = pathExpr(sb.path, 'node');
|
|
458
|
-
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
459
|
-
lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
460
|
-
} else {
|
|
461
|
-
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
462
|
-
lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Attr bindings
|
|
467
|
-
for (const ab of forBlock.attrBindings) {
|
|
468
|
-
const nodeRef = pathExpr(ab.path, 'node');
|
|
469
|
-
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
470
|
-
lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
|
|
471
|
-
lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
472
|
-
} else {
|
|
473
|
-
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
474
|
-
lines.push(`${indent} __effect(() => {`);
|
|
475
|
-
lines.push(`${indent} const __val = ${expr};`);
|
|
476
|
-
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
477
|
-
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
478
|
-
lines.push(`${indent} });`);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Model bindings
|
|
483
|
-
for (const mb of (forBlock.modelBindings || [])) {
|
|
484
|
-
const nodeRef = pathExpr(mb.path, 'node');
|
|
485
|
-
lines.push(`${indent} __effect(() => {`);
|
|
486
|
-
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
487
|
-
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
488
|
-
} else if (mb.prop === 'checked') {
|
|
489
|
-
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
490
|
-
} else {
|
|
491
|
-
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
492
|
-
}
|
|
493
|
-
lines.push(`${indent} });`);
|
|
494
|
-
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
495
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
496
|
-
} else if (mb.coerce) {
|
|
497
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
498
|
-
} else {
|
|
499
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Scoped slots
|
|
504
|
-
for (const s of (forBlock.slots || [])) {
|
|
505
|
-
if (s.slotProps.length > 0) {
|
|
506
|
-
const slotNodeRef = pathExpr(s.path, 'node');
|
|
507
|
-
const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
|
|
508
|
-
lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
|
|
509
|
-
lines.push(`${indent} const __sp = { ${propsEntries} };`);
|
|
510
|
-
lines.push(`${indent} let __h = __slotEl.innerHTML;`);
|
|
511
|
-
lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
|
|
512
|
-
lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '(\\\\(\\\\))?\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
513
|
-
lines.push(`${indent} }`);
|
|
514
|
-
lines.push(`${indent} __slotEl.innerHTML = __h;`);
|
|
515
|
-
lines.push(`${indent} }`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Nested each directives (forBlocks)
|
|
520
|
-
for (const innerFor of (forBlock.forBlocks || [])) {
|
|
521
|
-
const innerVn = innerFor.varName;
|
|
522
|
-
const innerItemVar = innerFor.itemVar;
|
|
523
|
-
const innerIndexVar = innerFor.indexVar;
|
|
524
|
-
const innerSource = innerFor.source;
|
|
525
|
-
const innerKeyExpr = innerFor.keyExpr;
|
|
526
|
-
|
|
527
|
-
// Build excludeSet that includes BOTH outer and inner loop variables
|
|
528
|
-
// so transformForExpr does not rewrite them as signals
|
|
529
|
-
const outerExcludeVars = [itemVar];
|
|
530
|
-
if (indexVar) outerExcludeVars.push(indexVar);
|
|
531
|
-
const innerExcludeVars = [innerItemVar];
|
|
532
|
-
if (innerIndexVar) innerExcludeVars.push(innerIndexVar);
|
|
533
|
-
|
|
534
|
-
// Create inner template element
|
|
535
|
-
lines.push(`${indent} const ${innerVn}_tpl = document.createElement('template');`);
|
|
536
|
-
lines.push(`${indent} ${innerVn}_tpl.innerHTML = \`${innerFor.templateHtml}\`;`);
|
|
537
|
-
|
|
538
|
-
// Find inner anchor comment in the cloned outer item node
|
|
539
|
-
lines.push(`${indent} const ${innerVn}_anchor = ${pathExpr(innerFor.anchorPath, 'node')};`);
|
|
540
|
-
|
|
541
|
-
// Transform the inner source expression (may reference outer item var)
|
|
542
|
-
const innerSourceExpr = transformForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
543
|
-
|
|
544
|
-
// Determine if inner source is static (only references outer loop vars)
|
|
545
|
-
const innerSourceIsStatic = isStaticForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
546
|
-
|
|
547
|
-
if (innerKeyExpr) {
|
|
548
|
-
// ── Keyed reconciliation for nested each ──
|
|
549
|
-
lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
|
|
550
|
-
lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
|
|
551
|
-
lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
|
|
552
|
-
lines.push(`${indent} : (${innerVn}_source || []);`);
|
|
553
|
-
lines.push(`${indent} const ${innerVn}_newNodes = [];`);
|
|
554
|
-
lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
|
|
555
|
-
lines.push(`${indent} const __key = ${innerKeyExpr};`);
|
|
556
|
-
lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
|
|
557
|
-
lines.push(`${indent} const innerNode = clone.firstChild;`);
|
|
558
|
-
|
|
559
|
-
// Generate inner item bindings with combined excludeSet
|
|
560
|
-
generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
|
|
561
|
-
|
|
562
|
-
lines.push(`${indent} ${innerVn}_newNodes.push(innerNode);`);
|
|
563
|
-
lines.push(`${indent} });`);
|
|
564
|
-
lines.push(`${indent} for (const n of ${innerVn}_newNodes) { ${innerVn}_anchor.parentNode.insertBefore(n, ${innerVn}_anchor); }`);
|
|
565
|
-
} else {
|
|
566
|
-
// ── Non-keyed nested each: iterate and clone ──
|
|
567
|
-
lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
|
|
568
|
-
lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
|
|
569
|
-
lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
|
|
570
|
-
lines.push(`${indent} : (${innerVn}_source || []);`);
|
|
571
|
-
lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
|
|
572
|
-
lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
|
|
573
|
-
lines.push(`${indent} const innerNode = clone.firstChild;`);
|
|
574
|
-
|
|
575
|
-
// Generate inner item bindings with combined excludeSet
|
|
576
|
-
generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
|
|
577
|
-
|
|
578
|
-
lines.push(`${indent} ${innerVn}_anchor.parentNode.insertBefore(innerNode, ${innerVn}_anchor);`);
|
|
579
|
-
lines.push(`${indent} });`);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Nested if/else-if/else chains (ifBlocks)
|
|
584
|
-
for (const ifBlock of (forBlock.ifBlocks || [])) {
|
|
585
|
-
const vn = ifBlock.varName;
|
|
586
|
-
const branches = ifBlock.branches;
|
|
587
|
-
|
|
588
|
-
// 3.1: Create template elements for each branch
|
|
589
|
-
for (let i = 0; i < branches.length; i++) {
|
|
590
|
-
const branch = branches[i];
|
|
591
|
-
lines.push(`${indent} const ${vn}_t${i} = document.createElement('template');`);
|
|
592
|
-
lines.push(`${indent} ${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// 3.1: Find anchor comment in the cloned node
|
|
596
|
-
lines.push(`${indent} const ${vn}_anchor = ${pathExpr(ifBlock.anchorPath, 'node')};`);
|
|
597
|
-
|
|
598
|
-
// 3.2: Generate per-item conditional evaluation (static, not reactive)
|
|
599
|
-
lines.push(`${indent} let ${vn}_branch = null;`);
|
|
600
|
-
for (let i = 0; i < branches.length; i++) {
|
|
601
|
-
const branch = branches[i];
|
|
602
|
-
if (branch.type === 'if') {
|
|
603
|
-
lines.push(`${indent} if (${branch.expression}) { ${vn}_branch = ${i}; }`);
|
|
604
|
-
} else if (branch.type === 'else-if') {
|
|
605
|
-
lines.push(`${indent} else if (${branch.expression}) { ${vn}_branch = ${i}; }`);
|
|
606
|
-
} else {
|
|
607
|
-
// else
|
|
608
|
-
lines.push(`${indent} else { ${vn}_branch = ${i}; }`);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// 3.3: Insert only the matching branch node and apply branch bindings/events/show/attr/model
|
|
613
|
-
lines.push(`${indent} if (${vn}_branch !== null) {`);
|
|
614
|
-
const tplArray = branches.map((_, i) => `${vn}_t${i}`).join(', ');
|
|
615
|
-
lines.push(`${indent} const ${vn}_tpl = [${tplArray}][${vn}_branch];`);
|
|
616
|
-
lines.push(`${indent} const ${vn}_clone = ${vn}_tpl.content.cloneNode(true);`);
|
|
617
|
-
lines.push(`${indent} const ${vn}_node = ${vn}_clone.firstChild;`);
|
|
618
|
-
lines.push(`${indent} ${vn}_anchor.parentNode.insertBefore(${vn}_node, ${vn}_anchor);`);
|
|
619
|
-
|
|
620
|
-
// Apply branch bindings/events/show/attr/model using the outer loop's item variable
|
|
621
|
-
const hasSetup = branches.some(b =>
|
|
622
|
-
(b.bindings && b.bindings.length > 0) ||
|
|
623
|
-
(b.events && b.events.length > 0) ||
|
|
624
|
-
(b.showBindings && b.showBindings.length > 0) ||
|
|
625
|
-
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
626
|
-
(b.modelBindings && b.modelBindings.length > 0)
|
|
627
|
-
);
|
|
628
|
-
if (hasSetup) {
|
|
629
|
-
// Generate per-branch setup inline (static evaluation using item variable)
|
|
630
|
-
for (let i = 0; i < branches.length; i++) {
|
|
631
|
-
const branch = branches[i];
|
|
632
|
-
const hasBranchSetup =
|
|
633
|
-
(branch.bindings && branch.bindings.length > 0) ||
|
|
634
|
-
(branch.events && branch.events.length > 0) ||
|
|
635
|
-
(branch.showBindings && branch.showBindings.length > 0) ||
|
|
636
|
-
(branch.attrBindings && branch.attrBindings.length > 0) ||
|
|
637
|
-
(branch.modelBindings && branch.modelBindings.length > 0);
|
|
638
|
-
if (!hasBranchSetup) continue;
|
|
639
|
-
|
|
640
|
-
const keyword = i === 0 ? 'if' : 'else if';
|
|
641
|
-
lines.push(`${indent} ${keyword} (${vn}_branch === ${i}) {`);
|
|
642
|
-
|
|
643
|
-
// Bindings (static: use item var directly)
|
|
644
|
-
for (const b of branch.bindings) {
|
|
645
|
-
const nodeRef = pathExpr(b.path, `${vn}_node`);
|
|
646
|
-
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
647
|
-
lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
648
|
-
} else {
|
|
649
|
-
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
650
|
-
lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// Events
|
|
655
|
-
for (const e of branch.events) {
|
|
656
|
-
const nodeRef = pathExpr(e.path, `${vn}_node`);
|
|
657
|
-
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
658
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Show bindings
|
|
662
|
-
for (const sb of (branch.showBindings || [])) {
|
|
663
|
-
const nodeRef = pathExpr(sb.path, `${vn}_node`);
|
|
664
|
-
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
665
|
-
lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
666
|
-
} else {
|
|
667
|
-
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
668
|
-
lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Attr bindings
|
|
673
|
-
for (const ab of (branch.attrBindings || [])) {
|
|
674
|
-
const nodeRef = pathExpr(ab.path, `${vn}_node`);
|
|
675
|
-
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
676
|
-
lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
|
|
677
|
-
lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
678
|
-
} else {
|
|
679
|
-
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
680
|
-
lines.push(`${indent} __effect(() => {`);
|
|
681
|
-
lines.push(`${indent} const __val = ${expr};`);
|
|
682
|
-
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
683
|
-
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
684
|
-
lines.push(`${indent} });`);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Model bindings
|
|
689
|
-
for (const mb of (branch.modelBindings || [])) {
|
|
690
|
-
const nodeRef = pathExpr(mb.path, `${vn}_node`);
|
|
691
|
-
lines.push(`${indent} __effect(() => {`);
|
|
692
|
-
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
693
|
-
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
694
|
-
} else if (mb.prop === 'checked') {
|
|
695
|
-
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
696
|
-
} else {
|
|
697
|
-
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
698
|
-
}
|
|
699
|
-
lines.push(`${indent} });`);
|
|
700
|
-
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
701
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
702
|
-
} else if (mb.coerce) {
|
|
703
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
704
|
-
} else {
|
|
705
|
-
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
lines.push(`${indent} }`);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
lines.push(`${indent} }`);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Generate inner item bindings/events/show/attr/model for a nested each directive.
|
|
718
|
-
* Uses transformForExpr with an excludeSet that includes BOTH outer and inner loop variables.
|
|
719
|
-
*
|
|
720
|
-
* @param {string[]} lines - Output lines array
|
|
721
|
-
* @param {ForBlock} innerFor - The nested ForBlock
|
|
722
|
-
* @param {string} outerItemVar - Outer loop item variable
|
|
723
|
-
* @param {string|null} outerIndexVar - Outer loop index variable
|
|
724
|
-
* @param {string} innerItemVar - Inner loop item variable
|
|
725
|
-
* @param {string|null} innerIndexVar - Inner loop index variable
|
|
726
|
-
* @param {Set<string>} propNames - Prop names set
|
|
727
|
-
* @param {Set<string>} signalNamesSet - Signal names set
|
|
728
|
-
* @param {Set<string>} computedNamesSet - Computed names set
|
|
729
|
-
* @param {string} indent - Current indentation
|
|
730
|
-
*/
|
|
731
|
-
function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent) {
|
|
732
|
-
// Build combined exclude set with both outer and inner loop variables
|
|
733
|
-
const combinedExcludeItemVar = innerItemVar;
|
|
734
|
-
const combinedExcludeIndexVar = innerIndexVar;
|
|
735
|
-
|
|
736
|
-
// For transformForExpr, we need to ensure both outer and inner vars are excluded.
|
|
737
|
-
// We create a modified propNames/signalNamesSet/computedNamesSet that doesn't include
|
|
738
|
-
// any of the loop variables. transformForExpr already excludes itemVar/indexVar,
|
|
739
|
-
// but we also need to exclude the outer loop variables.
|
|
740
|
-
// Strategy: filter out outer loop vars from the sets passed to transformForExpr
|
|
741
|
-
const filteredSignalNames = new Set([...signalNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
742
|
-
const filteredComputedNames = new Set([...computedNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
743
|
-
const filteredPropNames = new Set([...propNames].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
744
|
-
|
|
745
|
-
// Helper: check if expression is static (only references inner/outer loop vars, no signals/computeds/props)
|
|
746
|
-
function isNestedStatic(expr) {
|
|
747
|
-
// An expression is static if it only references the loop variables (outer + inner)
|
|
748
|
-
const allExclude = new Set([innerItemVar, outerItemVar]);
|
|
749
|
-
if (innerIndexVar) allExclude.add(innerIndexVar);
|
|
750
|
-
if (outerIndexVar) allExclude.add(outerIndexVar);
|
|
751
|
-
|
|
752
|
-
for (const p of propNames) {
|
|
753
|
-
if (allExclude.has(p)) continue;
|
|
754
|
-
if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
|
|
755
|
-
}
|
|
756
|
-
for (const n of signalNamesSet) {
|
|
757
|
-
if (allExclude.has(n)) continue;
|
|
758
|
-
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
759
|
-
}
|
|
760
|
-
for (const n of computedNamesSet) {
|
|
761
|
-
if (allExclude.has(n)) continue;
|
|
762
|
-
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
763
|
-
}
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Helper: transform expression excluding both outer and inner loop vars
|
|
768
|
-
function transformNested(expr) {
|
|
769
|
-
return transformForExpr(expr, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// Bindings
|
|
773
|
-
for (const b of innerFor.bindings) {
|
|
774
|
-
const nodeRef = pathExpr(b.path, 'innerNode');
|
|
775
|
-
if (isNestedStatic(b.name)) {
|
|
776
|
-
lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
777
|
-
} else {
|
|
778
|
-
const expr = transformNested(b.name);
|
|
779
|
-
lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Events
|
|
784
|
-
for (const e of innerFor.events) {
|
|
785
|
-
const nodeRef = pathExpr(e.path, 'innerNode');
|
|
786
|
-
const handlerExpr = generateForEventHandler(e.handler, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
|
|
787
|
-
lines.push(`${indent}${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// Show
|
|
791
|
-
for (const sb of innerFor.showBindings) {
|
|
792
|
-
const nodeRef = pathExpr(sb.path, 'innerNode');
|
|
793
|
-
if (isNestedStatic(sb.expression)) {
|
|
794
|
-
lines.push(`${indent}${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
795
|
-
} else {
|
|
796
|
-
const expr = transformNested(sb.expression);
|
|
797
|
-
lines.push(`${indent}__effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Attr bindings
|
|
802
|
-
for (const ab of innerFor.attrBindings) {
|
|
803
|
-
const nodeRef = pathExpr(ab.path, 'innerNode');
|
|
804
|
-
if (isNestedStatic(ab.expression)) {
|
|
805
|
-
lines.push(`${indent}const __val_${ab.varName} = ${ab.expression};`);
|
|
806
|
-
lines.push(`${indent}if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
807
|
-
} else {
|
|
808
|
-
const expr = transformNested(ab.expression);
|
|
809
|
-
lines.push(`${indent}__effect(() => {`);
|
|
810
|
-
lines.push(`${indent} const __val = ${expr};`);
|
|
811
|
-
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
812
|
-
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
813
|
-
lines.push(`${indent}});`);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// Model bindings
|
|
818
|
-
for (const mb of (innerFor.modelBindings || [])) {
|
|
819
|
-
const nodeRef = pathExpr(mb.path, 'innerNode');
|
|
820
|
-
lines.push(`${indent}__effect(() => {`);
|
|
821
|
-
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
822
|
-
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
823
|
-
} else if (mb.prop === 'checked') {
|
|
824
|
-
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
825
|
-
} else {
|
|
826
|
-
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
827
|
-
}
|
|
828
|
-
lines.push(`${indent}});`);
|
|
829
|
-
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
830
|
-
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
831
|
-
} else if (mb.coerce) {
|
|
832
|
-
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
833
|
-
} else {
|
|
834
|
-
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* Generate a fully self-contained JS component from a ParseResult.
|
|
841
|
-
*
|
|
842
|
-
* @param {ParseResult} parseResult — Complete IR with bindings/events
|
|
843
|
-
* @param {{ runtimeImportPath?: string }} [options] — Optional generation options
|
|
844
|
-
* @returns {string} JavaScript source code
|
|
845
|
-
*/
|
|
846
|
-
export function generateComponent(parseResult, options = {}) {
|
|
847
|
-
const {
|
|
848
|
-
tagName,
|
|
849
|
-
className,
|
|
850
|
-
style,
|
|
851
|
-
signals,
|
|
852
|
-
computeds,
|
|
853
|
-
effects,
|
|
854
|
-
methods,
|
|
855
|
-
bindings,
|
|
856
|
-
events,
|
|
857
|
-
processedTemplate,
|
|
858
|
-
propDefs = [],
|
|
859
|
-
propsObjectName = null,
|
|
860
|
-
emits = [],
|
|
861
|
-
emitsObjectName = null,
|
|
862
|
-
ifBlocks = [],
|
|
863
|
-
showBindings = [],
|
|
864
|
-
forBlocks = [],
|
|
865
|
-
onMountHooks = [],
|
|
866
|
-
onDestroyHooks = [],
|
|
867
|
-
onAdoptHooks = [],
|
|
868
|
-
modelBindings = [],
|
|
869
|
-
modelPropBindings = [],
|
|
870
|
-
attrBindings = [],
|
|
871
|
-
slots = [],
|
|
872
|
-
constantVars = [],
|
|
873
|
-
watchers = [],
|
|
874
|
-
refs = [],
|
|
875
|
-
refBindings = [],
|
|
876
|
-
childComponents = [],
|
|
877
|
-
childImports = [],
|
|
878
|
-
exposeNames = [],
|
|
879
|
-
modelDefs = [],
|
|
880
|
-
dynamicComponents = [],
|
|
881
|
-
} = parseResult;
|
|
882
|
-
|
|
883
|
-
const signalNames = signals.map(s => s.name);
|
|
884
|
-
const computedNames = computeds.map(c => c.name);
|
|
885
|
-
const constantNames = constantVars.map(v => v.name);
|
|
886
|
-
const methodNames = methods.map(m => m.name);
|
|
887
|
-
const refVarNames = refs.map(r => r.varName);
|
|
888
|
-
const propNames = new Set(propDefs.map(p => p.name));
|
|
889
|
-
|
|
890
|
-
// Build model var name → prop name map for transform functions
|
|
891
|
-
const modelVarMap = new Map();
|
|
892
|
-
for (const md of modelDefs) {
|
|
893
|
-
modelVarMap.set(md.varName, md.name);
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const lines = [];
|
|
897
|
-
const comment = options.comments ? (text) => lines.push(` // --- ${text} ---`) : () => {};
|
|
898
|
-
|
|
899
|
-
// ── 0. Source comment ──
|
|
900
|
-
if (options.sourceFile) {
|
|
901
|
-
lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// ── 1. Reactive runtime (shared import or inline) ──
|
|
905
|
-
if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
|
|
906
|
-
// Determine which runtime functions this component needs
|
|
907
|
-
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || dynamicComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
908
|
-
const needsComputed = computeds.length > 0;
|
|
909
|
-
const needsUntrack = watchers.length > 0;
|
|
910
|
-
|
|
911
|
-
if (options.runtimeImportPath) {
|
|
912
|
-
// Tree-shake: only import what this component actually uses
|
|
913
|
-
const usedRuntime = new Set(['__signal']); // always need __signal
|
|
914
|
-
if (needsComputed) usedRuntime.add('__computed');
|
|
915
|
-
if (needsEffect) usedRuntime.add('__effect');
|
|
916
|
-
if (needsUntrack) usedRuntime.add('__untrack');
|
|
917
|
-
const imports = [...usedRuntime].join(', ');
|
|
918
|
-
lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
|
|
919
|
-
} else {
|
|
920
|
-
// Standalone: inline only the runtime functions this component needs
|
|
921
|
-
lines.push(buildInlineRuntime({ needsComputed, needsEffect, needsBatch: false, needsUntrack }).trim());
|
|
922
|
-
}
|
|
923
|
-
lines.push('');
|
|
924
|
-
|
|
925
|
-
// ── 1b. Child component imports ──
|
|
926
|
-
for (const ci of childImports) {
|
|
927
|
-
if (ci.sideEffect) {
|
|
928
|
-
// Side-effect import: no identifier, child self-registers
|
|
929
|
-
lines.push(`import '${ci.importPath}';`);
|
|
930
|
-
} else {
|
|
931
|
-
// Named import with guarded registration
|
|
932
|
-
lines.push(`import ${ci.identifier} from '${ci.importPath}';`);
|
|
933
|
-
lines.push(`if (!customElements.get(${ci.identifier}.__meta.tag)) customElements.define(${ci.identifier}.__meta.tag, ${ci.identifier});`);
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
if (childImports.length > 0) {
|
|
937
|
-
lines.push('');
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// ── 2. CSS injection (scoped, deduplicated via id guard) ──
|
|
941
|
-
if (style) {
|
|
942
|
-
if (options.comments) lines.push('// ── Styles ───────────────────────────────────────────');
|
|
943
|
-
const scoped = scopeCSS(style, tagName);
|
|
944
|
-
const cssId = `__css_${className}`;
|
|
945
|
-
lines.push(`if (!document.getElementById('${cssId}')) {`);
|
|
946
|
-
lines.push(` const ${cssId} = document.createElement('style');`);
|
|
947
|
-
lines.push(` ${cssId}.id = '${cssId}';`);
|
|
948
|
-
lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
|
|
949
|
-
lines.push(` document.head.appendChild(${cssId});`);
|
|
950
|
-
lines.push('}');
|
|
951
|
-
lines.push('');
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// ── 3. Template element ──
|
|
955
|
-
if (options.comments) lines.push('// ── Template ─────────────────────────────────────────');
|
|
956
|
-
lines.push(`const __t_${className} = document.createElement('template');`);
|
|
957
|
-
lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
|
|
958
|
-
lines.push('');
|
|
959
|
-
|
|
960
|
-
// ── 4. HTMLElement class ──
|
|
961
|
-
if (options.comments) lines.push('// ── Component ────────────────────────────────────────');
|
|
962
|
-
lines.push(`class ${className} extends HTMLElement {`);
|
|
963
|
-
|
|
964
|
-
// Static observedAttributes (if props or model props exist)
|
|
965
|
-
const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
|
|
966
|
-
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
967
|
-
const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
|
|
968
|
-
// For model props, observe BOTH kebab-case AND camelCase forms
|
|
969
|
-
// Vue sets camelCase (modelValue), native HTML uses kebab-case (model-value)
|
|
970
|
-
const modelAttrEntries = [];
|
|
971
|
-
for (let i = 0; i < modelDefs.length; i++) {
|
|
972
|
-
const kebab = modelAttrNames[i];
|
|
973
|
-
const camel = modelDefs[i].name;
|
|
974
|
-
modelAttrEntries.push(`'${kebab}'`);
|
|
975
|
-
// Only add camelCase if it differs from kebab-case
|
|
976
|
-
if (kebab !== camel) {
|
|
977
|
-
modelAttrEntries.push(`'${camel}'`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
|
|
981
|
-
lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
|
|
982
|
-
lines.push('');
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// Static __scopedSlots array (lists slot names with reactive props)
|
|
986
|
-
const scopedSlotNames = slots.filter(s => s.name && s.slotProps.length > 0).map(s => s.name);
|
|
987
|
-
if (scopedSlotNames.length > 0) {
|
|
988
|
-
const scopedArr = scopedSlotNames.map(n => `'${n}'`).join(', ');
|
|
989
|
-
lines.push(` static __scopedSlots = [${scopedArr}];`);
|
|
990
|
-
lines.push('');
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Static __meta — component metadata for framework adapters (React wrappers, Angular events, etc.)
|
|
994
|
-
{
|
|
995
|
-
const metaProps = propDefs.map(p => `{ name: '${p.name}', default: ${p.default} }`).join(', ');
|
|
996
|
-
const metaEvents = emits.map(e => `'${e}'`).join(', ');
|
|
997
|
-
const metaModels = modelDefs.map(m => `'${m.name}'`).join(', ');
|
|
998
|
-
const metaSlots = slots.filter(s => s.name).map(s => `'${s.name}'`).join(', ');
|
|
999
|
-
lines.push(` static __meta = { tag: '${tagName}', props: [${metaProps}], events: [${metaEvents}], models: [${metaModels}], slots: [${metaSlots}] };`);
|
|
1000
|
-
lines.push('');
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
|
|
1004
|
-
lines.push(' constructor() {');
|
|
1005
|
-
lines.push(' super();');
|
|
1006
|
-
|
|
1007
|
-
// Scoped slot storage initialization
|
|
1008
|
-
if (scopedSlotNames.length > 0) {
|
|
1009
|
-
lines.push(' this.__slotRenderers = {};');
|
|
1010
|
-
lines.push(' this.__slotProps = {};');
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
// Prop signal initialization (BEFORE user signals)
|
|
1014
|
-
for (const p of propDefs) {
|
|
1015
|
-
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
// Signal initialization
|
|
1019
|
-
for (const s of signals) {
|
|
1020
|
-
if (s === signals[0]) comment('Signals');
|
|
1021
|
-
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Model signal initialization
|
|
1025
|
-
for (const md of modelDefs) {
|
|
1026
|
-
lines.push(` this._m_${md.name} = __signal(${md.default});`);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Constant initialization
|
|
1030
|
-
for (const c of constantVars) {
|
|
1031
|
-
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Computed initialization
|
|
1035
|
-
for (const c of computeds) {
|
|
1036
|
-
if (c === computeds[0]) comment('Computed');
|
|
1037
|
-
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1038
|
-
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Watcher prev-value initialization
|
|
1042
|
-
for (let idx = 0; idx < watchers.length; idx++) {
|
|
1043
|
-
const w = watchers[idx];
|
|
1044
|
-
if (w.kind === 'signal') {
|
|
1045
|
-
// For signal watchers watching a prop, initialize with the prop's default value
|
|
1046
|
-
// so that attributeChangedCallback changes before connectedCallback are detected
|
|
1047
|
-
if (propNames.has(w.target)) {
|
|
1048
|
-
const propDef = propDefs.find(p => p.name === w.target);
|
|
1049
|
-
lines.push(` this.__prev_${w.target} = ${propDef ? propDef.default : 'undefined'};`);
|
|
1050
|
-
} else {
|
|
1051
|
-
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
1052
|
-
}
|
|
1053
|
-
} else {
|
|
1054
|
-
// For getter watchers, check if the getter references a prop (e.g., props.value)
|
|
1055
|
-
// Initialize with the prop's default so pre-connection attribute changes are detected
|
|
1056
|
-
const propMatch = propsObjectName ? w.target.match(new RegExp(`^${propsObjectName}\\.(\\w+)$`)) : null;
|
|
1057
|
-
if (propMatch && propNames.has(propMatch[1])) {
|
|
1058
|
-
const propDef = propDefs.find(p => p.name === propMatch[1]);
|
|
1059
|
-
lines.push(` this.__prev_watch${idx} = ${propDef ? propDef.default : 'undefined'};`);
|
|
1060
|
-
} else {
|
|
1061
|
-
lines.push(` this.__prev_watch${idx} = undefined;`);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
lines.push(' }');
|
|
1067
|
-
lines.push('');
|
|
1068
|
-
|
|
1069
|
-
// connectedCallback (idempotent — safe for re-mount)
|
|
1070
|
-
lines.push(' connectedCallback() {');
|
|
1071
|
-
lines.push(' if (this.__connected) return;');
|
|
1072
|
-
lines.push(' this.__connected = true;');
|
|
1073
|
-
|
|
1074
|
-
// ── DOM SETUP (moved from constructor for Custom Elements spec compliance) ──
|
|
1075
|
-
|
|
1076
|
-
// Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
|
|
1077
|
-
if (slots.length > 0) {
|
|
1078
|
-
lines.push(' const __slotMap = {};');
|
|
1079
|
-
lines.push(' const __defaultSlotNodes = [];');
|
|
1080
|
-
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
1081
|
-
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1082
|
-
lines.push(' for (const attr of child.attributes) {');
|
|
1083
|
-
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1084
|
-
lines.push(' const slotName = attr.name.slice(1);');
|
|
1085
|
-
lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
|
|
1086
|
-
lines.push(' }');
|
|
1087
|
-
lines.push(' }');
|
|
1088
|
-
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1089
|
-
// NEW: regular element with slot="name" (cross-framework support)
|
|
1090
|
-
lines.push(" const slotName = child.getAttribute('slot');");
|
|
1091
|
-
lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
|
|
1092
|
-
lines.push(" child.removeAttribute('slot');");
|
|
1093
|
-
lines.push(" child.removeAttribute('slot-props');");
|
|
1094
|
-
lines.push(" __slotMap[slotName] = { content: propsExpr ? child.innerHTML : child.outerHTML, propsExpr };");
|
|
1095
|
-
lines.push(" } else if (child.nodeType === 1) {");
|
|
1096
|
-
// NEW: check for slot-template-<name> attributes (React/Angular string attribute pattern)
|
|
1097
|
-
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1098
|
-
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1099
|
-
lines.push(" const slotName = attr.name.slice('slot-template-'.length);");
|
|
1100
|
-
lines.push(" if (!__slotMap[slotName]) {");
|
|
1101
|
-
lines.push(" __slotMap[slotName] = { content: attr.value, propsExpr: '' };");
|
|
1102
|
-
lines.push(" }");
|
|
1103
|
-
lines.push(" child.removeAttribute(attr.name);");
|
|
1104
|
-
lines.push(" }");
|
|
1105
|
-
lines.push(" }");
|
|
1106
|
-
lines.push(" __defaultSlotNodes.push(child);");
|
|
1107
|
-
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1108
|
-
lines.push(' __defaultSlotNodes.push(child);');
|
|
1109
|
-
lines.push(' }');
|
|
1110
|
-
lines.push(' }');
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// Clone template
|
|
1114
|
-
lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
|
|
1115
|
-
|
|
1116
|
-
// Assign DOM refs for bindings
|
|
1117
|
-
for (const b of bindings) {
|
|
1118
|
-
lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// Assign DOM refs for events
|
|
1122
|
-
for (const e of events) {
|
|
1123
|
-
lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Assign DOM refs for show bindings
|
|
1127
|
-
for (const sb of showBindings) {
|
|
1128
|
-
lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// Assign DOM refs for model bindings
|
|
1132
|
-
for (const mb of modelBindings) {
|
|
1133
|
-
lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
// Assign DOM refs for model:propName bindings
|
|
1137
|
-
for (const mpb of modelPropBindings) {
|
|
1138
|
-
lines.push(` this.${mpb.varName} = ${pathExpr(mpb.path, '__root')};`);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// Assign DOM refs for slot placeholders
|
|
1142
|
-
for (const s of slots) {
|
|
1143
|
-
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Assign DOM refs for child component instances (only if they have prop bindings)
|
|
1147
|
-
for (const cc of childComponents) {
|
|
1148
|
-
if (cc.propBindings.length > 0) {
|
|
1149
|
-
lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// Assign DOM refs for attr bindings (reuse ref when same path)
|
|
1154
|
-
const attrPathMap = new Map();
|
|
1155
|
-
for (const ab of attrBindings) {
|
|
1156
|
-
const pathKey = ab.path.join('.');
|
|
1157
|
-
if (attrPathMap.has(pathKey)) {
|
|
1158
|
-
lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
|
|
1159
|
-
} else {
|
|
1160
|
-
lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
|
|
1161
|
-
attrPathMap.set(pathKey, ab.varName);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// ── if: template creation, anchor reference, state init ──
|
|
1166
|
-
for (const ifBlock of ifBlocks) {
|
|
1167
|
-
const vn = ifBlock.varName;
|
|
1168
|
-
// Template per branch
|
|
1169
|
-
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
1170
|
-
const branch = ifBlock.branches[i];
|
|
1171
|
-
lines.push(` this.${vn}_t${i} = document.createElement('template');`);
|
|
1172
|
-
lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
1173
|
-
}
|
|
1174
|
-
// Reference to anchor comment node (must be before appendChild moves nodes out of __root)
|
|
1175
|
-
lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
|
|
1176
|
-
// Active branch state
|
|
1177
|
-
lines.push(` this.${vn}_current = null;`);
|
|
1178
|
-
lines.push(` this.${vn}_active = undefined;`);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// ── each: template creation, anchor reference, nodes array ──
|
|
1182
|
-
for (const forBlock of forBlocks) {
|
|
1183
|
-
const vn = forBlock.varName;
|
|
1184
|
-
lines.push(` this.${vn}_tpl = document.createElement('template');`);
|
|
1185
|
-
lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
|
|
1186
|
-
lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
|
|
1187
|
-
lines.push(` this.${vn}_nodes = [];`);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// ── dynamic component: anchor reference, state init ──
|
|
1191
|
-
for (const dyn of dynamicComponents) {
|
|
1192
|
-
const vn = dyn.varName;
|
|
1193
|
-
lines.push(` this.${vn}_anchor = ${pathExpr(dyn.anchorPath, '__root')};`);
|
|
1194
|
-
lines.push(` this.${vn}_current = null;`);
|
|
1195
|
-
lines.push(` this.${vn}_tag = null;`);
|
|
1196
|
-
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
1200
|
-
for (const rb of refBindings) {
|
|
1201
|
-
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Append DOM (always light DOM)
|
|
1205
|
-
lines.push(" this.innerHTML = '';");
|
|
1206
|
-
lines.push(' this.appendChild(__root);');
|
|
1207
|
-
|
|
1208
|
-
// Static slot injection (after DOM is appended)
|
|
1209
|
-
for (const s of slots) {
|
|
1210
|
-
if (s.name && s.slotProps.length > 0) {
|
|
1211
|
-
// Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
|
|
1212
|
-
lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
|
|
1213
|
-
if (s.defaultContent) {
|
|
1214
|
-
lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
|
|
1215
|
-
}
|
|
1216
|
-
} else if (s.name) {
|
|
1217
|
-
// Named slot: inject content directly
|
|
1218
|
-
lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
|
|
1219
|
-
} else {
|
|
1220
|
-
// Default slot: inject collected child nodes
|
|
1221
|
-
lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
// ── Deferred slot re-check (Angular compatibility) ──
|
|
1226
|
-
// Angular connects custom elements to DOM BEFORE projecting children.
|
|
1227
|
-
// If no slot content was found on first pass, schedule a microtask retry.
|
|
1228
|
-
// We save a reference to the rendered root node so the microtask can filter it out
|
|
1229
|
-
// and only process children that were projected by the framework after connectedCallback.
|
|
1230
|
-
if (slots.length > 0) {
|
|
1231
|
-
lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
|
|
1232
|
-
lines.push(' const __renderedRoot = this.firstElementChild;');
|
|
1233
|
-
lines.push(' queueMicrotask(() => {');
|
|
1234
|
-
lines.push(' const __sm = {};');
|
|
1235
|
-
lines.push(' const __dn = [];');
|
|
1236
|
-
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
1237
|
-
// Skip the rendered template root and any whitespace text nodes that were there before
|
|
1238
|
-
lines.push(' if (child === __renderedRoot) continue;');
|
|
1239
|
-
lines.push(' if (child.nodeType === 3 && !child.textContent.trim()) continue;');
|
|
1240
|
-
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1241
|
-
lines.push(' for (const attr of child.attributes) {');
|
|
1242
|
-
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1243
|
-
lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
|
|
1244
|
-
lines.push(' }');
|
|
1245
|
-
lines.push(' }');
|
|
1246
|
-
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1247
|
-
lines.push(" const sn = child.getAttribute('slot');");
|
|
1248
|
-
lines.push(" const pe = child.getAttribute('slot-props') || '';");
|
|
1249
|
-
lines.push(" child.removeAttribute('slot');");
|
|
1250
|
-
lines.push(" child.removeAttribute('slot-props');");
|
|
1251
|
-
lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
|
|
1252
|
-
lines.push(" child.remove();");
|
|
1253
|
-
lines.push(" } else if (child.nodeType === 1) {");
|
|
1254
|
-
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1255
|
-
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1256
|
-
lines.push(" const sn = attr.name.slice('slot-template-'.length);");
|
|
1257
|
-
lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
|
|
1258
|
-
lines.push(" child.removeAttribute(attr.name);");
|
|
1259
|
-
lines.push(" }");
|
|
1260
|
-
lines.push(" }");
|
|
1261
|
-
lines.push(" __dn.push(child);");
|
|
1262
|
-
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1263
|
-
lines.push(" __dn.push(child);");
|
|
1264
|
-
lines.push(' }');
|
|
1265
|
-
lines.push(' }');
|
|
1266
|
-
// Re-inject slots if we found content this time
|
|
1267
|
-
lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
|
|
1268
|
-
for (const s of slots) {
|
|
1269
|
-
if (s.name && s.slotProps.length > 0) {
|
|
1270
|
-
lines.push(` if (__sm['${s.name}']) {`);
|
|
1271
|
-
lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
|
|
1272
|
-
if (s.slotProps.length > 0 && s.slotProps[0].source) {
|
|
1273
|
-
lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
|
|
1274
|
-
}
|
|
1275
|
-
lines.push(` }`);
|
|
1276
|
-
} else if (s.name) {
|
|
1277
|
-
lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
|
|
1278
|
-
} else {
|
|
1279
|
-
lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
lines.push(' }');
|
|
1283
|
-
lines.push(' });');
|
|
1284
|
-
lines.push(' }');
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// ── EFFECTS AND LISTENERS ──
|
|
1288
|
-
lines.push(' this.__ac = new AbortController();');
|
|
1289
|
-
lines.push(' this.__disposers = [];');
|
|
1290
|
-
lines.push('');
|
|
1291
|
-
|
|
1292
|
-
// Binding effects — one __effect per binding
|
|
1293
|
-
if (bindings.length > 0) comment('Text bindings');
|
|
1294
|
-
for (const b of bindings) {
|
|
1295
|
-
if (b.type === 'prop') {
|
|
1296
|
-
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1297
|
-
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
1298
|
-
lines.push(' }));');
|
|
1299
|
-
} else if (b.type === 'signal') {
|
|
1300
|
-
// Check if this is a model var (needs _m_ prefix instead of _)
|
|
1301
|
-
const modelPropName = modelVarMap.get(b.name);
|
|
1302
|
-
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1303
|
-
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1304
|
-
lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
|
|
1305
|
-
lines.push(' }));');
|
|
1306
|
-
} else if (b.type === 'computed') {
|
|
1307
|
-
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1308
|
-
lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
|
|
1309
|
-
lines.push(' }));');
|
|
1310
|
-
} else {
|
|
1311
|
-
// method/expression type — check if it's a props.x access or a complex expression
|
|
1312
|
-
let ref;
|
|
1313
|
-
if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
|
|
1314
|
-
const propName = b.name.slice(propsObjectName.length + 1);
|
|
1315
|
-
ref = `this._s_${propName}()`;
|
|
1316
|
-
} else {
|
|
1317
|
-
//
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
//
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
lines.push(`
|
|
1341
|
-
//
|
|
1342
|
-
lines.push(`
|
|
1343
|
-
|
|
1344
|
-
lines.push(`
|
|
1345
|
-
//
|
|
1346
|
-
lines.push(`
|
|
1347
|
-
lines.push(
|
|
1348
|
-
lines.push(`
|
|
1349
|
-
|
|
1350
|
-
lines.push(` this
|
|
1351
|
-
lines.push(
|
|
1352
|
-
lines.push('
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
ref = `this._${pb.expr}()`;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
watchRef = `this.
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
1411
|
-
lines.push(
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
}
|
|
1430
|
-
lines.push(
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
lines.push(
|
|
1469
|
-
lines.push(
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
lines.push(
|
|
1474
|
-
lines.push(
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
lines.push(`
|
|
1513
|
-
lines.push(
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
lines.push(
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
lines.push('
|
|
1528
|
-
|
|
1529
|
-
lines.push('
|
|
1530
|
-
lines.push(` this.${ab.varName}
|
|
1531
|
-
lines.push(' }));');
|
|
1532
|
-
} else if (ab.kind === '
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
lines.push('
|
|
1540
|
-
lines.push(
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
lines.push('
|
|
1544
|
-
lines.push(
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
lines.push('
|
|
1555
|
-
lines.push(
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
lines.push('
|
|
1559
|
-
lines.push(
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
lines.push(
|
|
1588
|
-
|
|
1589
|
-
lines.push(`
|
|
1590
|
-
|
|
1591
|
-
lines.push('
|
|
1592
|
-
|
|
1593
|
-
lines.push(
|
|
1594
|
-
lines.push(
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
(b.
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
lines.push('
|
|
1627
|
-
lines.push(
|
|
1628
|
-
lines.push('');
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
lines.push(`
|
|
1637
|
-
lines.push(
|
|
1638
|
-
lines.push('
|
|
1639
|
-
lines.push('
|
|
1640
|
-
lines.push(
|
|
1641
|
-
lines.push(
|
|
1642
|
-
lines.push('
|
|
1643
|
-
lines.push('
|
|
1644
|
-
lines.push(
|
|
1645
|
-
lines.push('
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
lines
|
|
1654
|
-
|
|
1655
|
-
lines.push('');
|
|
1656
|
-
lines.push('
|
|
1657
|
-
lines.push('
|
|
1658
|
-
lines.push('');
|
|
1659
|
-
lines.push('
|
|
1660
|
-
lines.push(
|
|
1661
|
-
lines.push('');
|
|
1662
|
-
lines.push(
|
|
1663
|
-
lines.push(
|
|
1664
|
-
lines.push(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
lines.push(`
|
|
1668
|
-
lines.push(
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
lines.push(`
|
|
1672
|
-
lines.push(
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
lines.push(
|
|
1677
|
-
|
|
1678
|
-
lines
|
|
1679
|
-
|
|
1680
|
-
lines.push(
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
lines.push(
|
|
1693
|
-
lines.push(`
|
|
1694
|
-
lines.push(`
|
|
1695
|
-
lines.push(`
|
|
1696
|
-
lines.push(
|
|
1697
|
-
lines.push(
|
|
1698
|
-
lines.push(
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
lines.push(`
|
|
1716
|
-
lines.push('
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
//
|
|
1744
|
-
lines.push('
|
|
1745
|
-
lines.push('
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
lines.push('
|
|
1879
|
-
lines.push('
|
|
1880
|
-
lines.push('');
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
lines.push(`
|
|
1893
|
-
lines.push(`
|
|
1894
|
-
lines.push(`
|
|
1895
|
-
lines.push(`
|
|
1896
|
-
lines.push(`
|
|
1897
|
-
lines.push(
|
|
1898
|
-
lines.push(
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
lines.push('
|
|
1908
|
-
lines.push('
|
|
1909
|
-
lines.push('
|
|
1910
|
-
lines.push(' }');
|
|
1911
|
-
lines.push('
|
|
1912
|
-
lines.push('
|
|
1913
|
-
lines.push('
|
|
1914
|
-
lines.push('
|
|
1915
|
-
lines.push('
|
|
1916
|
-
lines.push('
|
|
1917
|
-
lines.push('');
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
lines.push(` ${name}(
|
|
1952
|
-
} else if (
|
|
1953
|
-
lines.push(` get ${name}() { return this.
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
(b.
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
(branch.
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
lines.push(` __effect(() => { ${b.varName}.textContent = ${
|
|
1993
|
-
} else if (b.type === '
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
lines.push(`
|
|
2030
|
-
lines.push(`
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
lines.push(` ${mb.varName}.
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
lines.push(
|
|
2068
|
-
lines.push('');
|
|
2069
|
-
|
|
2070
|
-
// ──
|
|
2071
|
-
lines.push(`
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Code Generator for wcCompiler v2.
|
|
3
|
+
*
|
|
4
|
+
* Takes a complete ParseResult (with bindings, events populated by tree-walker)
|
|
5
|
+
* and produces a self-contained JavaScript string with:
|
|
6
|
+
* - Inline mini reactive runtime (zero imports)
|
|
7
|
+
* - Scoped CSS injection
|
|
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.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { reactiveRuntime, buildInlineRuntime } from './reactive-runtime.js';
|
|
17
|
+
import { scopeCSS } from './css-scoper.js';
|
|
18
|
+
import { camelToKebab } from './parser-extractors.js';
|
|
19
|
+
|
|
20
|
+
/** @import { ParseResult } from './types.js' */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a path array to a JS expression string.
|
|
24
|
+
* e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
|
|
25
|
+
*
|
|
26
|
+
* @param {string[]} parts
|
|
27
|
+
* @param {string} rootVar
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function pathExpr(parts, rootVar) {
|
|
31
|
+
return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escape special regex characters in a string.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} str
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
function escapeRegex(str) {
|
|
41
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the signal reference for a slot prop source expression.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} source — Source variable name from :prop="source"
|
|
48
|
+
* @param {string[]} signalNames — Signal variable names
|
|
49
|
+
* @param {string[]} computedNames — Computed variable names
|
|
50
|
+
* @param {Set<string>} propNames — Prop names from defineProps
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function slotPropRef(source, signalNames, computedNames, propNames) {
|
|
54
|
+
if (propNames.has(source)) return `this._s_${source}()`;
|
|
55
|
+
if (computedNames.includes(source)) return `this._c_${source}()`;
|
|
56
|
+
if (signalNames.includes(source)) return `this._${source}()`;
|
|
57
|
+
return `'${source}'`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Transform an expression by rewriting signal/computed variable references
|
|
62
|
+
* to use `this._x()` / `this._c_x()` syntax for auto-unwrapping.
|
|
63
|
+
*
|
|
64
|
+
* Also handles `propsObjectName.propName` → `this._s_propName()` transformation.
|
|
65
|
+
* Also handles `emitsObjectName(` → `this._emit(` transformation.
|
|
66
|
+
*
|
|
67
|
+
* Uses word-boundary regex for each known signal/computed name.
|
|
68
|
+
* Does NOT transform if the name is followed by `.set(` (that's a write,
|
|
69
|
+
* handled by transformMethodBody).
|
|
70
|
+
*
|
|
71
|
+
* @param {string} expr — Expression to transform
|
|
72
|
+
* @param {string[]} signalNames — Signal variable names
|
|
73
|
+
* @param {string[]} computedNames — Computed variable names
|
|
74
|
+
* @param {string|null} [propsObjectName] — Props object variable name
|
|
75
|
+
* @param {Set<string>} [propNames] — Set of prop names
|
|
76
|
+
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function transformExpr(expr, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, constantNames = [], methodNames = [], modelVarMap = new Map()) {
|
|
80
|
+
let result = expr;
|
|
81
|
+
|
|
82
|
+
// Transform emit calls: emitsObjectName( → this._emit(
|
|
83
|
+
if (emitsObjectName) {
|
|
84
|
+
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
85
|
+
result = result.replace(emitsRe, 'this._emit(');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Transform method calls: methodName( → this._methodName(
|
|
89
|
+
for (const name of methodNames) {
|
|
90
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
91
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
92
|
+
const methodRe = new RegExp(`\\b${name}\\(`, 'g');
|
|
93
|
+
result = result.replace(methodRe, `this._${name}(`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Transform props.x → this._s_x() BEFORE signal/computed transforms
|
|
97
|
+
if (propsObjectName && propNames.size > 0) {
|
|
98
|
+
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
99
|
+
result = result.replace(propsRe, (match, propName) => {
|
|
100
|
+
if (propNames.has(propName)) {
|
|
101
|
+
return `this._s_${propName}()`;
|
|
102
|
+
}
|
|
103
|
+
return match; // leave unknown props unchanged
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Transform bare prop names → this._s_x() (for template expressions like :style="{ color: myProp }")
|
|
108
|
+
for (const propName of propNames) {
|
|
109
|
+
if (propsObjectName && propName === propsObjectName) continue;
|
|
110
|
+
if (emitsObjectName && propName === emitsObjectName) continue;
|
|
111
|
+
const bareRe = new RegExp(`\\b(${propName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
112
|
+
result = result.replace(bareRe, `this._s_${propName}()`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Transform model signal reads: varName() → this._m_{propName}() (BEFORE regular signals)
|
|
116
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
117
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
118
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
119
|
+
// First: transform varName() calls → this._m_propName()
|
|
120
|
+
const callRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
121
|
+
result = result.replace(callRe, `this._m_${propNameVal}()`);
|
|
122
|
+
// Then: transform bare varName references (not followed by ( or .set()) → this._m_propName()
|
|
123
|
+
const bareRe = new RegExp(`\\b(${varName})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
124
|
+
result = result.replace(bareRe, `this._m_${propNameVal}()`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Transform computed names first (to avoid partial matches with signals)
|
|
128
|
+
for (const name of computedNames) {
|
|
129
|
+
// Skip propsObjectName and emitsObjectName
|
|
130
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
131
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
132
|
+
// First: transform name() calls → this._c_name() (replace the call, not just the name)
|
|
133
|
+
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
134
|
+
result = result.replace(callRe, `this._c_${name}()`);
|
|
135
|
+
// Then: transform bare name references (not followed by ( or .set() → this._c_name()
|
|
136
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
137
|
+
result = result.replace(bareRe, `this._c_${name}()`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Transform signal names
|
|
141
|
+
for (const name of signalNames) {
|
|
142
|
+
// Skip propsObjectName and emitsObjectName
|
|
143
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
144
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
145
|
+
// First: transform name() calls → this._name() (replace the call, not just the name)
|
|
146
|
+
const callRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
147
|
+
result = result.replace(callRe, `this._${name}()`);
|
|
148
|
+
// Then: transform bare name references (not followed by ( or .set() → this._name()
|
|
149
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
150
|
+
result = result.replace(bareRe, `this._${name}()`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Transform constant names → this._const_name (no function call)
|
|
154
|
+
for (const name of constantNames) {
|
|
155
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
156
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
157
|
+
const bareRe = new RegExp(`\\b(${name})\\b(?!\\.set\\()(?!\\()`, 'g');
|
|
158
|
+
result = result.replace(bareRe, `this._const_${name}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Transform a method/effect body by rewriting signal writes and reads.
|
|
166
|
+
*
|
|
167
|
+
* - `emitsObjectName(` → `this._emit(` (emit call)
|
|
168
|
+
* - `props.x` → `this._s_x()` (prop access)
|
|
169
|
+
* - `varName.set(value)` → `this._modelSet_{propName}(value)` (model signal write)
|
|
170
|
+
* - `x.set(value)` → `this._x(value)` (signal write via setter)
|
|
171
|
+
* - `varName()` → `this._m_{propName}()` (model signal read)
|
|
172
|
+
* - `x()` → `this._x()` (signal read)
|
|
173
|
+
* - Computed `x()` → `this._c_x()` (computed read)
|
|
174
|
+
*
|
|
175
|
+
* @param {string} body — Function body to transform
|
|
176
|
+
* @param {string[]} signalNames — Signal variable names
|
|
177
|
+
* @param {string[]} computedNames — Computed variable names
|
|
178
|
+
* @param {string|null} [propsObjectName] — Props object variable name
|
|
179
|
+
* @param {Set<string>} [propNames] — Set of prop names
|
|
180
|
+
* @param {string|null} [emitsObjectName] — Emits object variable name
|
|
181
|
+
* @param {string[]} [refVarNames] — Ref variable names from templateRef declarations
|
|
182
|
+
* @param {string[]} [constantNames] — Constant variable names
|
|
183
|
+
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function transformMethodBody(body, signalNames, computedNames, propsObjectName = null, propNames = new Set(), emitsObjectName = null, refVarNames = [], constantNames = [], modelVarMap = new Map()) {
|
|
187
|
+
let result = body;
|
|
188
|
+
|
|
189
|
+
// 0a. Transform emit calls: emitsObjectName( → this._emit(
|
|
190
|
+
if (emitsObjectName) {
|
|
191
|
+
const emitsRe = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(`, 'g');
|
|
192
|
+
result = result.replace(emitsRe, 'this._emit(');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 0b. Transform props.x → this._s_x() BEFORE other transforms
|
|
196
|
+
if (propsObjectName && propNames.size > 0) {
|
|
197
|
+
const propsRe = new RegExp(`\\b${propsObjectName}\\.(\\w+)`, 'g');
|
|
198
|
+
result = result.replace(propsRe, (match, propName) => {
|
|
199
|
+
if (propNames.has(propName)) {
|
|
200
|
+
return `this._s_${propName}()`;
|
|
201
|
+
}
|
|
202
|
+
return match;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 0c. Transform ref access: varName.value → this._varName.value
|
|
207
|
+
for (const name of refVarNames) {
|
|
208
|
+
const refRe = new RegExp(`\\b${name}\\.value\\b`, 'g');
|
|
209
|
+
result = result.replace(refRe, `this._${name}.value`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 0d. Transform model signal writes: varName.set(expr) → this._modelSet_{propName}(expr)
|
|
213
|
+
// Must run BEFORE regular signal .set() transforms
|
|
214
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
215
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
216
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
217
|
+
const setRe = new RegExp(`\\b${varName}\\.set\\(`, 'g');
|
|
218
|
+
result = result.replace(setRe, `this._modelSet_${propNameVal}(`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 1. Transform signal writes: x.set(value) → this._x(value)
|
|
222
|
+
for (const name of signalNames) {
|
|
223
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
224
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
225
|
+
const setRe = new RegExp(`\\b${name}\\.set\\(`, 'g');
|
|
226
|
+
result = result.replace(setRe, `this._${name}(`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 1b. Transform model signal reads: varName() → this._m_{propName}()
|
|
230
|
+
// Must run BEFORE regular signal read transforms
|
|
231
|
+
for (const [varName, propNameVal] of modelVarMap) {
|
|
232
|
+
if (propsObjectName && varName === propsObjectName) continue;
|
|
233
|
+
if (emitsObjectName && varName === emitsObjectName) continue;
|
|
234
|
+
const readRe = new RegExp(`\\b${varName}\\(\\)`, 'g');
|
|
235
|
+
result = result.replace(readRe, `this._m_${propNameVal}()`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 2. Transform computed reads: x() → this._c_x()
|
|
239
|
+
for (const name of computedNames) {
|
|
240
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
241
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
242
|
+
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
243
|
+
result = result.replace(readRe, `this._c_${name}()`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 3. Transform signal reads: x() → this._x()
|
|
247
|
+
for (const name of signalNames) {
|
|
248
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
249
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
250
|
+
const readRe = new RegExp(`\\b${name}\\(\\)`, 'g');
|
|
251
|
+
result = result.replace(readRe, `this._${name}()`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 4. Transform constant reads: name → this._const_name
|
|
255
|
+
for (const name of constantNames) {
|
|
256
|
+
if (propsObjectName && name === propsObjectName) continue;
|
|
257
|
+
if (emitsObjectName && name === emitsObjectName) continue;
|
|
258
|
+
const bareRe = new RegExp(`\\b${name}\\b(?!\\()`, 'g');
|
|
259
|
+
result = result.replace(bareRe, `this._const_${name}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Transform an expression within the scope of an each block.
|
|
267
|
+
* - References to itemVar and indexVar are left UNTRANSFORMED
|
|
268
|
+
* - References to component variables (props, reactive, computed) ARE transformed
|
|
269
|
+
*
|
|
270
|
+
* @param {string} expr - The expression to transform
|
|
271
|
+
* @param {string} itemVar - Name of the iteration variable
|
|
272
|
+
* @param {string | null} indexVar - Name of the index variable
|
|
273
|
+
* @param {Set<string>} propsSet
|
|
274
|
+
* @param {Set<string>} rootVarNames - Set of signal names
|
|
275
|
+
* @param {Set<string>} computedNames
|
|
276
|
+
* @returns {string}
|
|
277
|
+
*/
|
|
278
|
+
export function transformForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
279
|
+
let r = expr;
|
|
280
|
+
const excludeSet = new Set([itemVar]);
|
|
281
|
+
if (indexVar) excludeSet.add(indexVar);
|
|
282
|
+
|
|
283
|
+
for (const p of propsSet) {
|
|
284
|
+
if (excludeSet.has(p)) continue;
|
|
285
|
+
// First: transform name() calls → this._s_name() (don't double-call)
|
|
286
|
+
r = r.replace(new RegExp(`\\b${p}\\(\\)`, 'g'), `this._s_${p}()`);
|
|
287
|
+
// Then: transform bare name references
|
|
288
|
+
r = r.replace(new RegExp(`\\b${p}\\b(?!\\()`, 'g'), `this._s_${p}()`);
|
|
289
|
+
}
|
|
290
|
+
for (const n of rootVarNames) {
|
|
291
|
+
if (excludeSet.has(n)) continue;
|
|
292
|
+
// First: transform name() calls → this._name() (don't double-call)
|
|
293
|
+
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._${n}()`);
|
|
294
|
+
// Then: transform bare name references
|
|
295
|
+
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._${n}()`);
|
|
296
|
+
}
|
|
297
|
+
for (const n of computedNames) {
|
|
298
|
+
if (excludeSet.has(n)) continue;
|
|
299
|
+
// First: transform name() calls → this._c_name() (don't double-call)
|
|
300
|
+
r = r.replace(new RegExp(`\\b${n}\\(\\)`, 'g'), `this._c_${n}()`);
|
|
301
|
+
// Then: transform bare name references
|
|
302
|
+
r = r.replace(new RegExp(`\\b${n}\\b(?!\\()`, 'g'), `this._c_${n}()`);
|
|
303
|
+
}
|
|
304
|
+
return r;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if a binding name is static within an each scope (references only item/index).
|
|
309
|
+
* A binding is static if it starts with itemVar + "." or equals itemVar or indexVar.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} name - The binding name (e.g. 'item.name', 'index', 'title')
|
|
312
|
+
* @param {string} itemVar
|
|
313
|
+
* @param {string | null} indexVar
|
|
314
|
+
* @returns {boolean}
|
|
315
|
+
*/
|
|
316
|
+
export function isStaticForBinding(name, itemVar, indexVar) {
|
|
317
|
+
if (name === itemVar || name.startsWith(itemVar + '.')) return true;
|
|
318
|
+
if (indexVar && name === indexVar) return true;
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if an expression is static within an each scope (references only item/index, no component vars).
|
|
324
|
+
*
|
|
325
|
+
* @param {string} expr
|
|
326
|
+
* @param {string} itemVar
|
|
327
|
+
* @param {string | null} indexVar
|
|
328
|
+
* @param {Set<string>} propsSet
|
|
329
|
+
* @param {Set<string>} rootVarNames
|
|
330
|
+
* @param {Set<string>} computedNames
|
|
331
|
+
* @returns {boolean}
|
|
332
|
+
*/
|
|
333
|
+
export function isStaticForExpr(expr, itemVar, indexVar, propsSet, rootVarNames, computedNames) {
|
|
334
|
+
const excludeSet = new Set([itemVar]);
|
|
335
|
+
if (indexVar) excludeSet.add(indexVar);
|
|
336
|
+
|
|
337
|
+
for (const p of propsSet) {
|
|
338
|
+
if (excludeSet.has(p)) continue;
|
|
339
|
+
if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
|
|
340
|
+
}
|
|
341
|
+
for (const n of rootVarNames) {
|
|
342
|
+
if (excludeSet.has(n)) continue;
|
|
343
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
344
|
+
}
|
|
345
|
+
for (const n of computedNames) {
|
|
346
|
+
if (excludeSet.has(n)) continue;
|
|
347
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Generate the JS expression for an event handler based on its type:
|
|
354
|
+
* - Simple name (e.g. "removeItem") → this._removeItem.bind(this)
|
|
355
|
+
* - Function call (e.g. "removeItem(item)") → (e) => { this._removeItem(item); }
|
|
356
|
+
* - Arrow function (e.g. "() => removeItem(item)") → () => { removeItem(item); }
|
|
357
|
+
*
|
|
358
|
+
* @param {string} handler — The raw handler string from the template
|
|
359
|
+
* @param {string[]} signalNames
|
|
360
|
+
* @param {string[]} computedNames
|
|
361
|
+
* @param {string|null} propsObjectName
|
|
362
|
+
* @param {Set<string>} propNames
|
|
363
|
+
* @param {string|null} emitsObjectName
|
|
364
|
+
* @param {string[]} constantNames
|
|
365
|
+
* @param {Map<string,string>} [modelVarMap] — Map from model varName → propName
|
|
366
|
+
* @returns {string}
|
|
367
|
+
*/
|
|
368
|
+
export function generateEventHandler(handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap = new Map()) {
|
|
369
|
+
if (handler.includes('=>')) {
|
|
370
|
+
// Arrow function expression: (e) => removeItem(item)
|
|
371
|
+
const arrowIdx = handler.indexOf('=>');
|
|
372
|
+
const params = handler.slice(0, arrowIdx).trim();
|
|
373
|
+
let body = handler.slice(arrowIdx + 2).trim();
|
|
374
|
+
body = transformMethodBody(body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, [], constantNames, modelVarMap);
|
|
375
|
+
return `${params} => { ${body}; }`;
|
|
376
|
+
} else if (handler.includes('(')) {
|
|
377
|
+
// Function call expression: removeItem(item)
|
|
378
|
+
const parenIdx = handler.indexOf('(');
|
|
379
|
+
const fnName = handler.slice(0, parenIdx).trim();
|
|
380
|
+
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
381
|
+
const transformedArgs = args ? transformExpr(args, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap) : '';
|
|
382
|
+
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
383
|
+
} else {
|
|
384
|
+
// Simple method name
|
|
385
|
+
return `this._${handler}.bind(this)`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Generate the JS expression for an event handler inside an each block.
|
|
391
|
+
* Similar to generateEventHandler but uses transformForExpr for the each scope.
|
|
392
|
+
*
|
|
393
|
+
* @param {string} handler
|
|
394
|
+
* @param {string} itemVar
|
|
395
|
+
* @param {string|null} indexVar
|
|
396
|
+
* @param {Set<string>} propNames
|
|
397
|
+
* @param {Set<string>} signalNamesSet
|
|
398
|
+
* @param {Set<string>} computedNamesSet
|
|
399
|
+
* @returns {string}
|
|
400
|
+
*/
|
|
401
|
+
export function generateForEventHandler(handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
402
|
+
if (handler.includes('=>')) {
|
|
403
|
+
// Arrow function expression
|
|
404
|
+
const arrowIdx = handler.indexOf('=>');
|
|
405
|
+
const params = handler.slice(0, arrowIdx).trim();
|
|
406
|
+
let body = handler.slice(arrowIdx + 2).trim();
|
|
407
|
+
body = transformForExpr(body, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
408
|
+
return `${params} => { ${body}; }`;
|
|
409
|
+
} else if (handler.includes('(')) {
|
|
410
|
+
// Function call expression: removeItem(item)
|
|
411
|
+
const parenIdx = handler.indexOf('(');
|
|
412
|
+
const fnName = handler.slice(0, parenIdx).trim();
|
|
413
|
+
const args = handler.slice(parenIdx + 1, handler.lastIndexOf(')')).trim();
|
|
414
|
+
const transformedArgs = args ? transformForExpr(args, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) : '';
|
|
415
|
+
return `(e) => { this._${fnName}(${transformedArgs}); }`;
|
|
416
|
+
} else {
|
|
417
|
+
// Simple method name
|
|
418
|
+
return `this._${handler}.bind(this)`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Generate per-item setup code for bindings, events, show, attr, model, and slots.
|
|
424
|
+
* Used by both keyed and non-keyed each effects.
|
|
425
|
+
*
|
|
426
|
+
* @param {string[]} lines — Output lines array
|
|
427
|
+
* @param {object} forBlock — ForBlock with bindings, events, etc.
|
|
428
|
+
* @param {string} itemVar
|
|
429
|
+
* @param {string|null} indexVar
|
|
430
|
+
* @param {Set<string>} propNames
|
|
431
|
+
* @param {Set<string>} signalNamesSet
|
|
432
|
+
* @param {Set<string>} computedNamesSet
|
|
433
|
+
*/
|
|
434
|
+
function generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet) {
|
|
435
|
+
const indent = ' ';
|
|
436
|
+
|
|
437
|
+
// Bindings
|
|
438
|
+
for (const b of forBlock.bindings) {
|
|
439
|
+
const nodeRef = pathExpr(b.path, 'node');
|
|
440
|
+
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
441
|
+
lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
442
|
+
} else {
|
|
443
|
+
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
444
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Events
|
|
449
|
+
for (const e of forBlock.events) {
|
|
450
|
+
const nodeRef = pathExpr(e.path, 'node');
|
|
451
|
+
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
452
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Show
|
|
456
|
+
for (const sb of forBlock.showBindings) {
|
|
457
|
+
const nodeRef = pathExpr(sb.path, 'node');
|
|
458
|
+
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
459
|
+
lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
460
|
+
} else {
|
|
461
|
+
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
462
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Attr bindings
|
|
467
|
+
for (const ab of forBlock.attrBindings) {
|
|
468
|
+
const nodeRef = pathExpr(ab.path, 'node');
|
|
469
|
+
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
470
|
+
lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
|
|
471
|
+
lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
472
|
+
} else {
|
|
473
|
+
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
474
|
+
lines.push(`${indent} __effect(() => {`);
|
|
475
|
+
lines.push(`${indent} const __val = ${expr};`);
|
|
476
|
+
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
477
|
+
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
478
|
+
lines.push(`${indent} });`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Model bindings
|
|
483
|
+
for (const mb of (forBlock.modelBindings || [])) {
|
|
484
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
485
|
+
lines.push(`${indent} __effect(() => {`);
|
|
486
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
487
|
+
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
488
|
+
} else if (mb.prop === 'checked') {
|
|
489
|
+
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
490
|
+
} else {
|
|
491
|
+
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
492
|
+
}
|
|
493
|
+
lines.push(`${indent} });`);
|
|
494
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
495
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
496
|
+
} else if (mb.coerce) {
|
|
497
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
498
|
+
} else {
|
|
499
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Scoped slots
|
|
504
|
+
for (const s of (forBlock.slots || [])) {
|
|
505
|
+
if (s.slotProps.length > 0) {
|
|
506
|
+
const slotNodeRef = pathExpr(s.path, 'node');
|
|
507
|
+
const propsEntries = s.slotProps.map(sp => `'${sp.prop}': ${sp.source}`).join(', ');
|
|
508
|
+
lines.push(`${indent} { const __slotEl = ${slotNodeRef};`);
|
|
509
|
+
lines.push(`${indent} const __sp = { ${propsEntries} };`);
|
|
510
|
+
lines.push(`${indent} let __h = __slotEl.innerHTML;`);
|
|
511
|
+
lines.push(`${indent} for (const [k, v] of Object.entries(__sp)) {`);
|
|
512
|
+
lines.push(`${indent} __h = __h.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '(\\\\(\\\\))?\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
|
|
513
|
+
lines.push(`${indent} }`);
|
|
514
|
+
lines.push(`${indent} __slotEl.innerHTML = __h;`);
|
|
515
|
+
lines.push(`${indent} }`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Nested each directives (forBlocks)
|
|
520
|
+
for (const innerFor of (forBlock.forBlocks || [])) {
|
|
521
|
+
const innerVn = innerFor.varName;
|
|
522
|
+
const innerItemVar = innerFor.itemVar;
|
|
523
|
+
const innerIndexVar = innerFor.indexVar;
|
|
524
|
+
const innerSource = innerFor.source;
|
|
525
|
+
const innerKeyExpr = innerFor.keyExpr;
|
|
526
|
+
|
|
527
|
+
// Build excludeSet that includes BOTH outer and inner loop variables
|
|
528
|
+
// so transformForExpr does not rewrite them as signals
|
|
529
|
+
const outerExcludeVars = [itemVar];
|
|
530
|
+
if (indexVar) outerExcludeVars.push(indexVar);
|
|
531
|
+
const innerExcludeVars = [innerItemVar];
|
|
532
|
+
if (innerIndexVar) innerExcludeVars.push(innerIndexVar);
|
|
533
|
+
|
|
534
|
+
// Create inner template element
|
|
535
|
+
lines.push(`${indent} const ${innerVn}_tpl = document.createElement('template');`);
|
|
536
|
+
lines.push(`${indent} ${innerVn}_tpl.innerHTML = \`${innerFor.templateHtml}\`;`);
|
|
537
|
+
|
|
538
|
+
// Find inner anchor comment in the cloned outer item node
|
|
539
|
+
lines.push(`${indent} const ${innerVn}_anchor = ${pathExpr(innerFor.anchorPath, 'node')};`);
|
|
540
|
+
|
|
541
|
+
// Transform the inner source expression (may reference outer item var)
|
|
542
|
+
const innerSourceExpr = transformForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
543
|
+
|
|
544
|
+
// Determine if inner source is static (only references outer loop vars)
|
|
545
|
+
const innerSourceIsStatic = isStaticForExpr(innerSource, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
546
|
+
|
|
547
|
+
if (innerKeyExpr) {
|
|
548
|
+
// ── Keyed reconciliation for nested each ──
|
|
549
|
+
lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
|
|
550
|
+
lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
|
|
551
|
+
lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
|
|
552
|
+
lines.push(`${indent} : (${innerVn}_source || []);`);
|
|
553
|
+
lines.push(`${indent} const ${innerVn}_newNodes = [];`);
|
|
554
|
+
lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
|
|
555
|
+
lines.push(`${indent} const __key = ${innerKeyExpr};`);
|
|
556
|
+
lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
|
|
557
|
+
lines.push(`${indent} const innerNode = clone.firstChild;`);
|
|
558
|
+
|
|
559
|
+
// Generate inner item bindings with combined excludeSet
|
|
560
|
+
generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
|
|
561
|
+
|
|
562
|
+
lines.push(`${indent} ${innerVn}_newNodes.push(innerNode);`);
|
|
563
|
+
lines.push(`${indent} });`);
|
|
564
|
+
lines.push(`${indent} for (const n of ${innerVn}_newNodes) { ${innerVn}_anchor.parentNode.insertBefore(n, ${innerVn}_anchor); }`);
|
|
565
|
+
} else {
|
|
566
|
+
// ── Non-keyed nested each: iterate and clone ──
|
|
567
|
+
lines.push(`${indent} const ${innerVn}_source = ${innerSourceIsStatic ? innerSource : innerSourceExpr};`);
|
|
568
|
+
lines.push(`${indent} const ${innerVn}_iter = typeof ${innerVn}_source === 'number'`);
|
|
569
|
+
lines.push(`${indent} ? Array.from({ length: ${innerVn}_source }, (_, i) => i + 1)`);
|
|
570
|
+
lines.push(`${indent} : (${innerVn}_source || []);`);
|
|
571
|
+
lines.push(`${indent} ${innerVn}_iter.forEach((${innerItemVar}, ${innerIndexVar || '__idx'}) => {`);
|
|
572
|
+
lines.push(`${indent} const clone = ${innerVn}_tpl.content.cloneNode(true);`);
|
|
573
|
+
lines.push(`${indent} const innerNode = clone.firstChild;`);
|
|
574
|
+
|
|
575
|
+
// Generate inner item bindings with combined excludeSet
|
|
576
|
+
generateNestedItemSetup(lines, innerFor, itemVar, indexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent + ' ');
|
|
577
|
+
|
|
578
|
+
lines.push(`${indent} ${innerVn}_anchor.parentNode.insertBefore(innerNode, ${innerVn}_anchor);`);
|
|
579
|
+
lines.push(`${indent} });`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Nested if/else-if/else chains (ifBlocks)
|
|
584
|
+
for (const ifBlock of (forBlock.ifBlocks || [])) {
|
|
585
|
+
const vn = ifBlock.varName;
|
|
586
|
+
const branches = ifBlock.branches;
|
|
587
|
+
|
|
588
|
+
// 3.1: Create template elements for each branch
|
|
589
|
+
for (let i = 0; i < branches.length; i++) {
|
|
590
|
+
const branch = branches[i];
|
|
591
|
+
lines.push(`${indent} const ${vn}_t${i} = document.createElement('template');`);
|
|
592
|
+
lines.push(`${indent} ${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 3.1: Find anchor comment in the cloned node
|
|
596
|
+
lines.push(`${indent} const ${vn}_anchor = ${pathExpr(ifBlock.anchorPath, 'node')};`);
|
|
597
|
+
|
|
598
|
+
// 3.2: Generate per-item conditional evaluation (static, not reactive)
|
|
599
|
+
lines.push(`${indent} let ${vn}_branch = null;`);
|
|
600
|
+
for (let i = 0; i < branches.length; i++) {
|
|
601
|
+
const branch = branches[i];
|
|
602
|
+
if (branch.type === 'if') {
|
|
603
|
+
lines.push(`${indent} if (${branch.expression}) { ${vn}_branch = ${i}; }`);
|
|
604
|
+
} else if (branch.type === 'else-if') {
|
|
605
|
+
lines.push(`${indent} else if (${branch.expression}) { ${vn}_branch = ${i}; }`);
|
|
606
|
+
} else {
|
|
607
|
+
// else
|
|
608
|
+
lines.push(`${indent} else { ${vn}_branch = ${i}; }`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 3.3: Insert only the matching branch node and apply branch bindings/events/show/attr/model
|
|
613
|
+
lines.push(`${indent} if (${vn}_branch !== null) {`);
|
|
614
|
+
const tplArray = branches.map((_, i) => `${vn}_t${i}`).join(', ');
|
|
615
|
+
lines.push(`${indent} const ${vn}_tpl = [${tplArray}][${vn}_branch];`);
|
|
616
|
+
lines.push(`${indent} const ${vn}_clone = ${vn}_tpl.content.cloneNode(true);`);
|
|
617
|
+
lines.push(`${indent} const ${vn}_node = ${vn}_clone.firstChild;`);
|
|
618
|
+
lines.push(`${indent} ${vn}_anchor.parentNode.insertBefore(${vn}_node, ${vn}_anchor);`);
|
|
619
|
+
|
|
620
|
+
// Apply branch bindings/events/show/attr/model using the outer loop's item variable
|
|
621
|
+
const hasSetup = branches.some(b =>
|
|
622
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
623
|
+
(b.events && b.events.length > 0) ||
|
|
624
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
625
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
626
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
627
|
+
);
|
|
628
|
+
if (hasSetup) {
|
|
629
|
+
// Generate per-branch setup inline (static evaluation using item variable)
|
|
630
|
+
for (let i = 0; i < branches.length; i++) {
|
|
631
|
+
const branch = branches[i];
|
|
632
|
+
const hasBranchSetup =
|
|
633
|
+
(branch.bindings && branch.bindings.length > 0) ||
|
|
634
|
+
(branch.events && branch.events.length > 0) ||
|
|
635
|
+
(branch.showBindings && branch.showBindings.length > 0) ||
|
|
636
|
+
(branch.attrBindings && branch.attrBindings.length > 0) ||
|
|
637
|
+
(branch.modelBindings && branch.modelBindings.length > 0);
|
|
638
|
+
if (!hasBranchSetup) continue;
|
|
639
|
+
|
|
640
|
+
const keyword = i === 0 ? 'if' : 'else if';
|
|
641
|
+
lines.push(`${indent} ${keyword} (${vn}_branch === ${i}) {`);
|
|
642
|
+
|
|
643
|
+
// Bindings (static: use item var directly)
|
|
644
|
+
for (const b of branch.bindings) {
|
|
645
|
+
const nodeRef = pathExpr(b.path, `${vn}_node`);
|
|
646
|
+
if (isStaticForBinding(b.name, itemVar, indexVar)) {
|
|
647
|
+
lines.push(`${indent} ${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
648
|
+
} else {
|
|
649
|
+
const expr = transformForExpr(b.name, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
650
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Events
|
|
655
|
+
for (const e of branch.events) {
|
|
656
|
+
const nodeRef = pathExpr(e.path, `${vn}_node`);
|
|
657
|
+
const handlerExpr = generateForEventHandler(e.handler, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
658
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Show bindings
|
|
662
|
+
for (const sb of (branch.showBindings || [])) {
|
|
663
|
+
const nodeRef = pathExpr(sb.path, `${vn}_node`);
|
|
664
|
+
if (isStaticForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
665
|
+
lines.push(`${indent} ${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
666
|
+
} else {
|
|
667
|
+
const expr = transformForExpr(sb.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
668
|
+
lines.push(`${indent} __effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Attr bindings
|
|
673
|
+
for (const ab of (branch.attrBindings || [])) {
|
|
674
|
+
const nodeRef = pathExpr(ab.path, `${vn}_node`);
|
|
675
|
+
if (isStaticForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet)) {
|
|
676
|
+
lines.push(`${indent} const __val_${ab.varName} = ${ab.expression};`);
|
|
677
|
+
lines.push(`${indent} if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
678
|
+
} else {
|
|
679
|
+
const expr = transformForExpr(ab.expression, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
680
|
+
lines.push(`${indent} __effect(() => {`);
|
|
681
|
+
lines.push(`${indent} const __val = ${expr};`);
|
|
682
|
+
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
683
|
+
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
684
|
+
lines.push(`${indent} });`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Model bindings
|
|
689
|
+
for (const mb of (branch.modelBindings || [])) {
|
|
690
|
+
const nodeRef = pathExpr(mb.path, `${vn}_node`);
|
|
691
|
+
lines.push(`${indent} __effect(() => {`);
|
|
692
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
693
|
+
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
694
|
+
} else if (mb.prop === 'checked') {
|
|
695
|
+
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
696
|
+
} else {
|
|
697
|
+
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
698
|
+
}
|
|
699
|
+
lines.push(`${indent} });`);
|
|
700
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
701
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
702
|
+
} else if (mb.coerce) {
|
|
703
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
704
|
+
} else {
|
|
705
|
+
lines.push(`${indent} ${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
lines.push(`${indent} }`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
lines.push(`${indent} }`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Generate inner item bindings/events/show/attr/model for a nested each directive.
|
|
718
|
+
* Uses transformForExpr with an excludeSet that includes BOTH outer and inner loop variables.
|
|
719
|
+
*
|
|
720
|
+
* @param {string[]} lines - Output lines array
|
|
721
|
+
* @param {ForBlock} innerFor - The nested ForBlock
|
|
722
|
+
* @param {string} outerItemVar - Outer loop item variable
|
|
723
|
+
* @param {string|null} outerIndexVar - Outer loop index variable
|
|
724
|
+
* @param {string} innerItemVar - Inner loop item variable
|
|
725
|
+
* @param {string|null} innerIndexVar - Inner loop index variable
|
|
726
|
+
* @param {Set<string>} propNames - Prop names set
|
|
727
|
+
* @param {Set<string>} signalNamesSet - Signal names set
|
|
728
|
+
* @param {Set<string>} computedNamesSet - Computed names set
|
|
729
|
+
* @param {string} indent - Current indentation
|
|
730
|
+
*/
|
|
731
|
+
function generateNestedItemSetup(lines, innerFor, outerItemVar, outerIndexVar, innerItemVar, innerIndexVar, propNames, signalNamesSet, computedNamesSet, indent) {
|
|
732
|
+
// Build combined exclude set with both outer and inner loop variables
|
|
733
|
+
const combinedExcludeItemVar = innerItemVar;
|
|
734
|
+
const combinedExcludeIndexVar = innerIndexVar;
|
|
735
|
+
|
|
736
|
+
// For transformForExpr, we need to ensure both outer and inner vars are excluded.
|
|
737
|
+
// We create a modified propNames/signalNamesSet/computedNamesSet that doesn't include
|
|
738
|
+
// any of the loop variables. transformForExpr already excludes itemVar/indexVar,
|
|
739
|
+
// but we also need to exclude the outer loop variables.
|
|
740
|
+
// Strategy: filter out outer loop vars from the sets passed to transformForExpr
|
|
741
|
+
const filteredSignalNames = new Set([...signalNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
742
|
+
const filteredComputedNames = new Set([...computedNamesSet].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
743
|
+
const filteredPropNames = new Set([...propNames].filter(n => n !== outerItemVar && n !== outerIndexVar));
|
|
744
|
+
|
|
745
|
+
// Helper: check if expression is static (only references inner/outer loop vars, no signals/computeds/props)
|
|
746
|
+
function isNestedStatic(expr) {
|
|
747
|
+
// An expression is static if it only references the loop variables (outer + inner)
|
|
748
|
+
const allExclude = new Set([innerItemVar, outerItemVar]);
|
|
749
|
+
if (innerIndexVar) allExclude.add(innerIndexVar);
|
|
750
|
+
if (outerIndexVar) allExclude.add(outerIndexVar);
|
|
751
|
+
|
|
752
|
+
for (const p of propNames) {
|
|
753
|
+
if (allExclude.has(p)) continue;
|
|
754
|
+
if (new RegExp(`\\b${p}\\b`).test(expr)) return false;
|
|
755
|
+
}
|
|
756
|
+
for (const n of signalNamesSet) {
|
|
757
|
+
if (allExclude.has(n)) continue;
|
|
758
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
759
|
+
}
|
|
760
|
+
for (const n of computedNamesSet) {
|
|
761
|
+
if (allExclude.has(n)) continue;
|
|
762
|
+
if (new RegExp(`\\b${n}\\b`).test(expr)) return false;
|
|
763
|
+
}
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Helper: transform expression excluding both outer and inner loop vars
|
|
768
|
+
function transformNested(expr) {
|
|
769
|
+
return transformForExpr(expr, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Bindings
|
|
773
|
+
for (const b of innerFor.bindings) {
|
|
774
|
+
const nodeRef = pathExpr(b.path, 'innerNode');
|
|
775
|
+
if (isNestedStatic(b.name)) {
|
|
776
|
+
lines.push(`${indent}${nodeRef}.textContent = ${b.name} ?? '';`);
|
|
777
|
+
} else {
|
|
778
|
+
const expr = transformNested(b.name);
|
|
779
|
+
lines.push(`${indent}__effect(() => { ${nodeRef}.textContent = ${expr} ?? ''; });`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Events
|
|
784
|
+
for (const e of innerFor.events) {
|
|
785
|
+
const nodeRef = pathExpr(e.path, 'innerNode');
|
|
786
|
+
const handlerExpr = generateForEventHandler(e.handler, innerItemVar, innerIndexVar, filteredPropNames, filteredSignalNames, filteredComputedNames);
|
|
787
|
+
lines.push(`${indent}${nodeRef}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Show
|
|
791
|
+
for (const sb of innerFor.showBindings) {
|
|
792
|
+
const nodeRef = pathExpr(sb.path, 'innerNode');
|
|
793
|
+
if (isNestedStatic(sb.expression)) {
|
|
794
|
+
lines.push(`${indent}${nodeRef}.style.display = (${sb.expression}) ? '' : 'none';`);
|
|
795
|
+
} else {
|
|
796
|
+
const expr = transformNested(sb.expression);
|
|
797
|
+
lines.push(`${indent}__effect(() => { ${nodeRef}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Attr bindings
|
|
802
|
+
for (const ab of innerFor.attrBindings) {
|
|
803
|
+
const nodeRef = pathExpr(ab.path, 'innerNode');
|
|
804
|
+
if (isNestedStatic(ab.expression)) {
|
|
805
|
+
lines.push(`${indent}const __val_${ab.varName} = ${ab.expression};`);
|
|
806
|
+
lines.push(`${indent}if (__val_${ab.varName} != null && __val_${ab.varName} !== false) { ${nodeRef}.setAttribute('${ab.attr}', __val_${ab.varName}); }`);
|
|
807
|
+
} else {
|
|
808
|
+
const expr = transformNested(ab.expression);
|
|
809
|
+
lines.push(`${indent}__effect(() => {`);
|
|
810
|
+
lines.push(`${indent} const __val = ${expr};`);
|
|
811
|
+
lines.push(`${indent} if (__val == null || __val === false) { ${nodeRef}.removeAttribute('${ab.attr}'); }`);
|
|
812
|
+
lines.push(`${indent} else { ${nodeRef}.setAttribute('${ab.attr}', __val); }`);
|
|
813
|
+
lines.push(`${indent}});`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Model bindings
|
|
818
|
+
for (const mb of (innerFor.modelBindings || [])) {
|
|
819
|
+
const nodeRef = pathExpr(mb.path, 'innerNode');
|
|
820
|
+
lines.push(`${indent}__effect(() => {`);
|
|
821
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
822
|
+
lines.push(`${indent} ${nodeRef}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
823
|
+
} else if (mb.prop === 'checked') {
|
|
824
|
+
lines.push(`${indent} ${nodeRef}.checked = !!this._${mb.signal}();`);
|
|
825
|
+
} else {
|
|
826
|
+
lines.push(`${indent} ${nodeRef}.value = this._${mb.signal}() ?? '';`);
|
|
827
|
+
}
|
|
828
|
+
lines.push(`${indent}});`);
|
|
829
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
830
|
+
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
831
|
+
} else if (mb.coerce) {
|
|
832
|
+
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
833
|
+
} else {
|
|
834
|
+
lines.push(`${indent}${nodeRef}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Generate a fully self-contained JS component from a ParseResult.
|
|
841
|
+
*
|
|
842
|
+
* @param {ParseResult} parseResult — Complete IR with bindings/events
|
|
843
|
+
* @param {{ runtimeImportPath?: string }} [options] — Optional generation options
|
|
844
|
+
* @returns {string} JavaScript source code
|
|
845
|
+
*/
|
|
846
|
+
export function generateComponent(parseResult, options = {}) {
|
|
847
|
+
const {
|
|
848
|
+
tagName,
|
|
849
|
+
className,
|
|
850
|
+
style,
|
|
851
|
+
signals,
|
|
852
|
+
computeds,
|
|
853
|
+
effects,
|
|
854
|
+
methods,
|
|
855
|
+
bindings,
|
|
856
|
+
events,
|
|
857
|
+
processedTemplate,
|
|
858
|
+
propDefs = [],
|
|
859
|
+
propsObjectName = null,
|
|
860
|
+
emits = [],
|
|
861
|
+
emitsObjectName = null,
|
|
862
|
+
ifBlocks = [],
|
|
863
|
+
showBindings = [],
|
|
864
|
+
forBlocks = [],
|
|
865
|
+
onMountHooks = [],
|
|
866
|
+
onDestroyHooks = [],
|
|
867
|
+
onAdoptHooks = [],
|
|
868
|
+
modelBindings = [],
|
|
869
|
+
modelPropBindings = [],
|
|
870
|
+
attrBindings = [],
|
|
871
|
+
slots = [],
|
|
872
|
+
constantVars = [],
|
|
873
|
+
watchers = [],
|
|
874
|
+
refs = [],
|
|
875
|
+
refBindings = [],
|
|
876
|
+
childComponents = [],
|
|
877
|
+
childImports = [],
|
|
878
|
+
exposeNames = [],
|
|
879
|
+
modelDefs = [],
|
|
880
|
+
dynamicComponents = [],
|
|
881
|
+
} = parseResult;
|
|
882
|
+
|
|
883
|
+
const signalNames = signals.map(s => s.name);
|
|
884
|
+
const computedNames = computeds.map(c => c.name);
|
|
885
|
+
const constantNames = constantVars.map(v => v.name);
|
|
886
|
+
const methodNames = methods.map(m => m.name);
|
|
887
|
+
const refVarNames = refs.map(r => r.varName);
|
|
888
|
+
const propNames = new Set(propDefs.map(p => p.name));
|
|
889
|
+
|
|
890
|
+
// Build model var name → prop name map for transform functions
|
|
891
|
+
const modelVarMap = new Map();
|
|
892
|
+
for (const md of modelDefs) {
|
|
893
|
+
modelVarMap.set(md.varName, md.name);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const lines = [];
|
|
897
|
+
const comment = options.comments ? (text) => lines.push(` // --- ${text} ---`) : () => {};
|
|
898
|
+
|
|
899
|
+
// ── 0. Source comment ──
|
|
900
|
+
if (options.sourceFile) {
|
|
901
|
+
lines.push(`// Generated from: ${options.sourceFile} (wcCompiler)`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ── 1. Reactive runtime (shared import or inline) ──
|
|
905
|
+
if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
|
|
906
|
+
// Determine which runtime functions this component needs
|
|
907
|
+
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || dynamicComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
908
|
+
const needsComputed = computeds.length > 0;
|
|
909
|
+
const needsUntrack = watchers.length > 0;
|
|
910
|
+
|
|
911
|
+
if (options.runtimeImportPath) {
|
|
912
|
+
// Tree-shake: only import what this component actually uses
|
|
913
|
+
const usedRuntime = new Set(['__signal']); // always need __signal
|
|
914
|
+
if (needsComputed) usedRuntime.add('__computed');
|
|
915
|
+
if (needsEffect) usedRuntime.add('__effect');
|
|
916
|
+
if (needsUntrack) usedRuntime.add('__untrack');
|
|
917
|
+
const imports = [...usedRuntime].join(', ');
|
|
918
|
+
lines.push(`import { ${imports} } from '${options.runtimeImportPath}';`);
|
|
919
|
+
} else {
|
|
920
|
+
// Standalone: inline only the runtime functions this component needs
|
|
921
|
+
lines.push(buildInlineRuntime({ needsComputed, needsEffect, needsBatch: false, needsUntrack }).trim());
|
|
922
|
+
}
|
|
923
|
+
lines.push('');
|
|
924
|
+
|
|
925
|
+
// ── 1b. Child component imports ──
|
|
926
|
+
for (const ci of childImports) {
|
|
927
|
+
if (ci.sideEffect) {
|
|
928
|
+
// Side-effect import: no identifier, child self-registers
|
|
929
|
+
lines.push(`import '${ci.importPath}';`);
|
|
930
|
+
} else {
|
|
931
|
+
// Named import with guarded registration
|
|
932
|
+
lines.push(`import ${ci.identifier} from '${ci.importPath}';`);
|
|
933
|
+
lines.push(`if (!customElements.get(${ci.identifier}.__meta.tag)) customElements.define(${ci.identifier}.__meta.tag, ${ci.identifier});`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (childImports.length > 0) {
|
|
937
|
+
lines.push('');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ── 2. CSS injection (scoped, deduplicated via id guard) ──
|
|
941
|
+
if (style) {
|
|
942
|
+
if (options.comments) lines.push('// ── Styles ───────────────────────────────────────────');
|
|
943
|
+
const scoped = scopeCSS(style, tagName);
|
|
944
|
+
const cssId = `__css_${className}`;
|
|
945
|
+
lines.push(`if (!document.getElementById('${cssId}')) {`);
|
|
946
|
+
lines.push(` const ${cssId} = document.createElement('style');`);
|
|
947
|
+
lines.push(` ${cssId}.id = '${cssId}';`);
|
|
948
|
+
lines.push(` ${cssId}.textContent = \`${scoped}\`;`);
|
|
949
|
+
lines.push(` document.head.appendChild(${cssId});`);
|
|
950
|
+
lines.push('}');
|
|
951
|
+
lines.push('');
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── 3. Template element ──
|
|
955
|
+
if (options.comments) lines.push('// ── Template ─────────────────────────────────────────');
|
|
956
|
+
lines.push(`const __t_${className} = document.createElement('template');`);
|
|
957
|
+
lines.push(`__t_${className}.innerHTML = \`${processedTemplate || ''}\`;`);
|
|
958
|
+
lines.push('');
|
|
959
|
+
|
|
960
|
+
// ── 4. HTMLElement class ──
|
|
961
|
+
if (options.comments) lines.push('// ── Component ────────────────────────────────────────');
|
|
962
|
+
lines.push(`class ${className} extends HTMLElement {`);
|
|
963
|
+
|
|
964
|
+
// Static observedAttributes (if props or model props exist)
|
|
965
|
+
const modelAttrNames = modelDefs.map(md => camelToKebab(md.name));
|
|
966
|
+
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
967
|
+
const propAttrNames = propDefs.map(p => `'${p.attrName}'`);
|
|
968
|
+
// For model props, observe BOTH kebab-case AND camelCase forms
|
|
969
|
+
// Vue sets camelCase (modelValue), native HTML uses kebab-case (model-value)
|
|
970
|
+
const modelAttrEntries = [];
|
|
971
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
972
|
+
const kebab = modelAttrNames[i];
|
|
973
|
+
const camel = modelDefs[i].name;
|
|
974
|
+
modelAttrEntries.push(`'${kebab}'`);
|
|
975
|
+
// Only add camelCase if it differs from kebab-case
|
|
976
|
+
if (kebab !== camel) {
|
|
977
|
+
modelAttrEntries.push(`'${camel}'`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
const allAttrNames = [...propAttrNames, ...modelAttrEntries].join(', ');
|
|
981
|
+
lines.push(` static get observedAttributes() { return [${allAttrNames}]; }`);
|
|
982
|
+
lines.push('');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Static __scopedSlots array (lists slot names with reactive props)
|
|
986
|
+
const scopedSlotNames = slots.filter(s => s.name && s.slotProps.length > 0).map(s => s.name);
|
|
987
|
+
if (scopedSlotNames.length > 0) {
|
|
988
|
+
const scopedArr = scopedSlotNames.map(n => `'${n}'`).join(', ');
|
|
989
|
+
lines.push(` static __scopedSlots = [${scopedArr}];`);
|
|
990
|
+
lines.push('');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Static __meta — component metadata for framework adapters (React wrappers, Angular events, etc.)
|
|
994
|
+
{
|
|
995
|
+
const metaProps = propDefs.map(p => `{ name: '${p.name}', default: ${p.default} }`).join(', ');
|
|
996
|
+
const metaEvents = emits.map(e => `'${e}'`).join(', ');
|
|
997
|
+
const metaModels = modelDefs.map(m => `'${m.name}'`).join(', ');
|
|
998
|
+
const metaSlots = slots.filter(s => s.name).map(s => `'${s.name}'`).join(', ');
|
|
999
|
+
lines.push(` static __meta = { tag: '${tagName}', props: [${metaProps}], events: [${metaEvents}], models: [${metaModels}], slots: [${metaSlots}] };`);
|
|
1000
|
+
lines.push('');
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
|
|
1004
|
+
lines.push(' constructor() {');
|
|
1005
|
+
lines.push(' super();');
|
|
1006
|
+
|
|
1007
|
+
// Scoped slot storage initialization
|
|
1008
|
+
if (scopedSlotNames.length > 0) {
|
|
1009
|
+
lines.push(' this.__slotRenderers = {};');
|
|
1010
|
+
lines.push(' this.__slotProps = {};');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Prop signal initialization (BEFORE user signals)
|
|
1014
|
+
for (const p of propDefs) {
|
|
1015
|
+
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Signal initialization
|
|
1019
|
+
for (const s of signals) {
|
|
1020
|
+
if (s === signals[0]) comment('Signals');
|
|
1021
|
+
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Model signal initialization
|
|
1025
|
+
for (const md of modelDefs) {
|
|
1026
|
+
lines.push(` this._m_${md.name} = __signal(${md.default});`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Constant initialization
|
|
1030
|
+
for (const c of constantVars) {
|
|
1031
|
+
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Computed initialization
|
|
1035
|
+
for (const c of computeds) {
|
|
1036
|
+
if (c === computeds[0]) comment('Computed');
|
|
1037
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1038
|
+
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Watcher prev-value initialization
|
|
1042
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
1043
|
+
const w = watchers[idx];
|
|
1044
|
+
if (w.kind === 'signal') {
|
|
1045
|
+
// For signal watchers watching a prop, initialize with the prop's default value
|
|
1046
|
+
// so that attributeChangedCallback changes before connectedCallback are detected
|
|
1047
|
+
if (propNames.has(w.target)) {
|
|
1048
|
+
const propDef = propDefs.find(p => p.name === w.target);
|
|
1049
|
+
lines.push(` this.__prev_${w.target} = ${propDef ? propDef.default : 'undefined'};`);
|
|
1050
|
+
} else {
|
|
1051
|
+
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
1052
|
+
}
|
|
1053
|
+
} else {
|
|
1054
|
+
// For getter watchers, check if the getter references a prop (e.g., props.value)
|
|
1055
|
+
// Initialize with the prop's default so pre-connection attribute changes are detected
|
|
1056
|
+
const propMatch = propsObjectName ? w.target.match(new RegExp(`^${propsObjectName}\\.(\\w+)$`)) : null;
|
|
1057
|
+
if (propMatch && propNames.has(propMatch[1])) {
|
|
1058
|
+
const propDef = propDefs.find(p => p.name === propMatch[1]);
|
|
1059
|
+
lines.push(` this.__prev_watch${idx} = ${propDef ? propDef.default : 'undefined'};`);
|
|
1060
|
+
} else {
|
|
1061
|
+
lines.push(` this.__prev_watch${idx} = undefined;`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
lines.push(' }');
|
|
1067
|
+
lines.push('');
|
|
1068
|
+
|
|
1069
|
+
// connectedCallback (idempotent — safe for re-mount)
|
|
1070
|
+
lines.push(' connectedCallback() {');
|
|
1071
|
+
lines.push(' if (this.__connected) return;');
|
|
1072
|
+
lines.push(' this.__connected = true;');
|
|
1073
|
+
|
|
1074
|
+
// ── DOM SETUP (moved from constructor for Custom Elements spec compliance) ──
|
|
1075
|
+
|
|
1076
|
+
// Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
|
|
1077
|
+
if (slots.length > 0) {
|
|
1078
|
+
lines.push(' const __slotMap = {};');
|
|
1079
|
+
lines.push(' const __defaultSlotNodes = [];');
|
|
1080
|
+
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
1081
|
+
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1082
|
+
lines.push(' for (const attr of child.attributes) {');
|
|
1083
|
+
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1084
|
+
lines.push(' const slotName = attr.name.slice(1);');
|
|
1085
|
+
lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
|
|
1086
|
+
lines.push(' }');
|
|
1087
|
+
lines.push(' }');
|
|
1088
|
+
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1089
|
+
// NEW: regular element with slot="name" (cross-framework support)
|
|
1090
|
+
lines.push(" const slotName = child.getAttribute('slot');");
|
|
1091
|
+
lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
|
|
1092
|
+
lines.push(" child.removeAttribute('slot');");
|
|
1093
|
+
lines.push(" child.removeAttribute('slot-props');");
|
|
1094
|
+
lines.push(" __slotMap[slotName] = { content: propsExpr ? child.innerHTML : child.outerHTML, propsExpr };");
|
|
1095
|
+
lines.push(" } else if (child.nodeType === 1) {");
|
|
1096
|
+
// NEW: check for slot-template-<name> attributes (React/Angular string attribute pattern)
|
|
1097
|
+
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1098
|
+
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1099
|
+
lines.push(" const slotName = attr.name.slice('slot-template-'.length);");
|
|
1100
|
+
lines.push(" if (!__slotMap[slotName]) {");
|
|
1101
|
+
lines.push(" __slotMap[slotName] = { content: attr.value, propsExpr: '' };");
|
|
1102
|
+
lines.push(" }");
|
|
1103
|
+
lines.push(" child.removeAttribute(attr.name);");
|
|
1104
|
+
lines.push(" }");
|
|
1105
|
+
lines.push(" }");
|
|
1106
|
+
lines.push(" __defaultSlotNodes.push(child);");
|
|
1107
|
+
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1108
|
+
lines.push(' __defaultSlotNodes.push(child);');
|
|
1109
|
+
lines.push(' }');
|
|
1110
|
+
lines.push(' }');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// Clone template
|
|
1114
|
+
lines.push(` const __root = __t_${className}.content.cloneNode(true);`);
|
|
1115
|
+
|
|
1116
|
+
// Assign DOM refs for bindings
|
|
1117
|
+
for (const b of bindings) {
|
|
1118
|
+
lines.push(` this.${b.varName} = ${pathExpr(b.path, '__root')};`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Assign DOM refs for events
|
|
1122
|
+
for (const e of events) {
|
|
1123
|
+
lines.push(` this.${e.varName} = ${pathExpr(e.path, '__root')};`);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Assign DOM refs for show bindings
|
|
1127
|
+
for (const sb of showBindings) {
|
|
1128
|
+
lines.push(` this.${sb.varName} = ${pathExpr(sb.path, '__root')};`);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Assign DOM refs for model bindings
|
|
1132
|
+
for (const mb of modelBindings) {
|
|
1133
|
+
lines.push(` this.${mb.varName} = ${pathExpr(mb.path, '__root')};`);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Assign DOM refs for model:propName bindings
|
|
1137
|
+
for (const mpb of modelPropBindings) {
|
|
1138
|
+
lines.push(` this.${mpb.varName} = ${pathExpr(mpb.path, '__root')};`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Assign DOM refs for slot placeholders
|
|
1142
|
+
for (const s of slots) {
|
|
1143
|
+
lines.push(` this.${s.varName} = ${pathExpr(s.path, '__root')};`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Assign DOM refs for child component instances (only if they have prop bindings)
|
|
1147
|
+
for (const cc of childComponents) {
|
|
1148
|
+
if (cc.propBindings.length > 0) {
|
|
1149
|
+
lines.push(` this.${cc.varName} = ${pathExpr(cc.path, '__root')};`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Assign DOM refs for attr bindings (reuse ref when same path)
|
|
1154
|
+
const attrPathMap = new Map();
|
|
1155
|
+
for (const ab of attrBindings) {
|
|
1156
|
+
const pathKey = ab.path.join('.');
|
|
1157
|
+
if (attrPathMap.has(pathKey)) {
|
|
1158
|
+
lines.push(` this.${ab.varName} = this.${attrPathMap.get(pathKey)};`);
|
|
1159
|
+
} else {
|
|
1160
|
+
lines.push(` this.${ab.varName} = ${pathExpr(ab.path, '__root')};`);
|
|
1161
|
+
attrPathMap.set(pathKey, ab.varName);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ── if: template creation, anchor reference, state init ──
|
|
1166
|
+
for (const ifBlock of ifBlocks) {
|
|
1167
|
+
const vn = ifBlock.varName;
|
|
1168
|
+
// Template per branch
|
|
1169
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
1170
|
+
const branch = ifBlock.branches[i];
|
|
1171
|
+
lines.push(` this.${vn}_t${i} = document.createElement('template');`);
|
|
1172
|
+
lines.push(` this.${vn}_t${i}.innerHTML = \`${branch.templateHtml}\`;`);
|
|
1173
|
+
}
|
|
1174
|
+
// Reference to anchor comment node (must be before appendChild moves nodes out of __root)
|
|
1175
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(ifBlock.anchorPath, '__root')};`);
|
|
1176
|
+
// Active branch state
|
|
1177
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1178
|
+
lines.push(` this.${vn}_active = undefined;`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
// ── each: template creation, anchor reference, nodes array ──
|
|
1182
|
+
for (const forBlock of forBlocks) {
|
|
1183
|
+
const vn = forBlock.varName;
|
|
1184
|
+
lines.push(` this.${vn}_tpl = document.createElement('template');`);
|
|
1185
|
+
lines.push(` this.${vn}_tpl.innerHTML = \`${forBlock.templateHtml}\`;`);
|
|
1186
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(forBlock.anchorPath, '__root')};`);
|
|
1187
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// ── dynamic component: anchor reference, state init ──
|
|
1191
|
+
for (const dyn of dynamicComponents) {
|
|
1192
|
+
const vn = dyn.varName;
|
|
1193
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(dyn.anchorPath, '__root')};`);
|
|
1194
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1195
|
+
lines.push(` this.${vn}_tag = null;`);
|
|
1196
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
1200
|
+
for (const rb of refBindings) {
|
|
1201
|
+
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Append DOM (always light DOM)
|
|
1205
|
+
lines.push(" this.innerHTML = '';");
|
|
1206
|
+
lines.push(' this.appendChild(__root);');
|
|
1207
|
+
|
|
1208
|
+
// Static slot injection (after DOM is appended)
|
|
1209
|
+
for (const s of slots) {
|
|
1210
|
+
if (s.name && s.slotProps.length > 0) {
|
|
1211
|
+
// Scoped slot: store consumer template or fallback for reactive effect in connectedCallback
|
|
1212
|
+
lines.push(` if (__slotMap['${s.name}']) { this.__slotTpl_${s.name} = __slotMap['${s.name}'].content; }`);
|
|
1213
|
+
if (s.defaultContent) {
|
|
1214
|
+
lines.push(` else { this.__slotTpl_${s.name} = \`${s.defaultContent}\`; }`);
|
|
1215
|
+
}
|
|
1216
|
+
} else if (s.name) {
|
|
1217
|
+
// Named slot: inject content directly
|
|
1218
|
+
lines.push(` if (__slotMap['${s.name}']) { this.${s.varName}.innerHTML = __slotMap['${s.name}'].content; }`);
|
|
1219
|
+
} else {
|
|
1220
|
+
// Default slot: inject collected child nodes
|
|
1221
|
+
lines.push(` if (__defaultSlotNodes.length) { this.${s.varName}.textContent = ''; __defaultSlotNodes.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ── Deferred slot re-check (Angular compatibility) ──
|
|
1226
|
+
// Angular connects custom elements to DOM BEFORE projecting children.
|
|
1227
|
+
// If no slot content was found on first pass, schedule a microtask retry.
|
|
1228
|
+
// We save a reference to the rendered root node so the microtask can filter it out
|
|
1229
|
+
// and only process children that were projected by the framework after connectedCallback.
|
|
1230
|
+
if (slots.length > 0) {
|
|
1231
|
+
lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
|
|
1232
|
+
lines.push(' const __renderedRoot = this.firstElementChild;');
|
|
1233
|
+
lines.push(' queueMicrotask(() => {');
|
|
1234
|
+
lines.push(' const __sm = {};');
|
|
1235
|
+
lines.push(' const __dn = [];');
|
|
1236
|
+
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
1237
|
+
// Skip the rendered template root and any whitespace text nodes that were there before
|
|
1238
|
+
lines.push(' if (child === __renderedRoot) continue;');
|
|
1239
|
+
lines.push(' if (child.nodeType === 3 && !child.textContent.trim()) continue;');
|
|
1240
|
+
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1241
|
+
lines.push(' for (const attr of child.attributes) {');
|
|
1242
|
+
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1243
|
+
lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
|
|
1244
|
+
lines.push(' }');
|
|
1245
|
+
lines.push(' }');
|
|
1246
|
+
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1247
|
+
lines.push(" const sn = child.getAttribute('slot');");
|
|
1248
|
+
lines.push(" const pe = child.getAttribute('slot-props') || '';");
|
|
1249
|
+
lines.push(" child.removeAttribute('slot');");
|
|
1250
|
+
lines.push(" child.removeAttribute('slot-props');");
|
|
1251
|
+
lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
|
|
1252
|
+
lines.push(" child.remove();");
|
|
1253
|
+
lines.push(" } else if (child.nodeType === 1) {");
|
|
1254
|
+
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1255
|
+
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1256
|
+
lines.push(" const sn = attr.name.slice('slot-template-'.length);");
|
|
1257
|
+
lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
|
|
1258
|
+
lines.push(" child.removeAttribute(attr.name);");
|
|
1259
|
+
lines.push(" }");
|
|
1260
|
+
lines.push(" }");
|
|
1261
|
+
lines.push(" __dn.push(child);");
|
|
1262
|
+
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1263
|
+
lines.push(" __dn.push(child);");
|
|
1264
|
+
lines.push(' }');
|
|
1265
|
+
lines.push(' }');
|
|
1266
|
+
// Re-inject slots if we found content this time
|
|
1267
|
+
lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
|
|
1268
|
+
for (const s of slots) {
|
|
1269
|
+
if (s.name && s.slotProps.length > 0) {
|
|
1270
|
+
lines.push(` if (__sm['${s.name}']) {`);
|
|
1271
|
+
lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
|
|
1272
|
+
if (s.slotProps.length > 0 && s.slotProps[0].source) {
|
|
1273
|
+
lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
|
|
1274
|
+
}
|
|
1275
|
+
lines.push(` }`);
|
|
1276
|
+
} else if (s.name) {
|
|
1277
|
+
lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
|
|
1278
|
+
} else {
|
|
1279
|
+
lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
lines.push(' }');
|
|
1283
|
+
lines.push(' });');
|
|
1284
|
+
lines.push(' }');
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ── EFFECTS AND LISTENERS ──
|
|
1288
|
+
lines.push(' this.__ac = new AbortController();');
|
|
1289
|
+
lines.push(' this.__disposers = [];');
|
|
1290
|
+
lines.push('');
|
|
1291
|
+
|
|
1292
|
+
// Binding effects — one __effect per binding
|
|
1293
|
+
if (bindings.length > 0) comment('Text bindings');
|
|
1294
|
+
for (const b of bindings) {
|
|
1295
|
+
if (b.type === 'prop') {
|
|
1296
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1297
|
+
lines.push(` this.${b.varName}.textContent = this._s_${b.name}() ?? '';`);
|
|
1298
|
+
lines.push(' }));');
|
|
1299
|
+
} else if (b.type === 'signal') {
|
|
1300
|
+
// Check if this is a model var (needs _m_ prefix instead of _)
|
|
1301
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1302
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1303
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1304
|
+
lines.push(` this.${b.varName}.textContent = ${signalRef} ?? '';`);
|
|
1305
|
+
lines.push(' }));');
|
|
1306
|
+
} else if (b.type === 'computed') {
|
|
1307
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1308
|
+
lines.push(` this.${b.varName}.textContent = this._c_${b.name}() ?? '';`);
|
|
1309
|
+
lines.push(' }));');
|
|
1310
|
+
} else {
|
|
1311
|
+
// method/expression type — check if it's a props.x access or a complex expression
|
|
1312
|
+
let ref;
|
|
1313
|
+
if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
|
|
1314
|
+
const propName = b.name.slice(propsObjectName.length + 1);
|
|
1315
|
+
ref = `this._s_${propName}()`;
|
|
1316
|
+
} else {
|
|
1317
|
+
// For method calls, ensure we have parentheses before transforming
|
|
1318
|
+
// b.name might be 'getCount' (from {{getCount()}} where baseName stripped the ())
|
|
1319
|
+
// We need to add () back so transformExpr can match and transform it
|
|
1320
|
+
const exprWithParens = b.name.includes('(') ? b.name : `${b.name}()`;
|
|
1321
|
+
// Use transformExpr for complex expressions (e.g. items().length, ternary)
|
|
1322
|
+
ref = transformExpr(exprWithParens, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1323
|
+
}
|
|
1324
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1325
|
+
lines.push(` this.${b.varName}.textContent = ${ref} ?? '';`);
|
|
1326
|
+
lines.push(' }));');
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Scoped slot effects — reactive resolution of {{propName}} in consumer templates
|
|
1331
|
+
for (const s of slots) {
|
|
1332
|
+
if (s.name && s.slotProps.length > 0) {
|
|
1333
|
+
const propsObj = s.slotProps.map(sp => {
|
|
1334
|
+
const ref = slotPropRef(sp.source, signalNames, computedNames, propNames);
|
|
1335
|
+
return `${sp.prop}: ${ref}`;
|
|
1336
|
+
}).join(', ');
|
|
1337
|
+
// Scoped slot effect: always compute props and notify renderers
|
|
1338
|
+
// The effect runs regardless of whether a template was provided (Angular uses registerSlotRenderer)
|
|
1339
|
+
lines.push(' __effect(() => {');
|
|
1340
|
+
lines.push(` const __props = { ${propsObj} };`);
|
|
1341
|
+
// Store current props for late-registering renderers
|
|
1342
|
+
lines.push(` this.__slotProps['${s.name}'] = __props;`);
|
|
1343
|
+
// Emit wcc:slot-update event
|
|
1344
|
+
lines.push(` this.dispatchEvent(new CustomEvent('wcc:slot-update', { detail: { slot: '${s.name}', props: __props }, bubbles: false }));`);
|
|
1345
|
+
// Check for registered renderer (Angular directive)
|
|
1346
|
+
lines.push(` if (this.__slotRenderers && this.__slotRenderers['${s.name}']) {`);
|
|
1347
|
+
lines.push(` this.__slotRenderers['${s.name}'](__props);`);
|
|
1348
|
+
lines.push(` } else if (this.__slotTpl_${s.name}) {`);
|
|
1349
|
+
// Fallback: template-based token replacement (WCC-to-WCC, Vue, React)
|
|
1350
|
+
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
1351
|
+
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
1352
|
+
lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
|
|
1353
|
+
lines.push(' }');
|
|
1354
|
+
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1355
|
+
lines.push(' }');
|
|
1356
|
+
lines.push(' });');
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Child component reactive prop bindings
|
|
1361
|
+
for (const cc of childComponents) {
|
|
1362
|
+
for (const pb of cc.propBindings) {
|
|
1363
|
+
let ref;
|
|
1364
|
+
if (pb.type === 'prop') {
|
|
1365
|
+
ref = `this._s_${pb.expr}()`;
|
|
1366
|
+
} else if (pb.type === 'computed') {
|
|
1367
|
+
ref = `this._c_${pb.expr}()`;
|
|
1368
|
+
} else if (pb.type === 'signal') {
|
|
1369
|
+
const modelPropName = modelVarMap.get(pb.expr);
|
|
1370
|
+
ref = modelPropName ? `this._m_${modelPropName}()` : `this._${pb.expr}()`;
|
|
1371
|
+
} else if (pb.type === 'constant') {
|
|
1372
|
+
ref = `this._const_${pb.expr}`;
|
|
1373
|
+
} else {
|
|
1374
|
+
ref = `this._${pb.expr}()`;
|
|
1375
|
+
}
|
|
1376
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1377
|
+
lines.push(` this.${cc.varName}.setAttribute('${pb.attr}', ${ref} ?? '');`);
|
|
1378
|
+
lines.push(' }));');
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// User effects
|
|
1383
|
+
for (const eff of effects) {
|
|
1384
|
+
const body = transformMethodBody(eff.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1385
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1386
|
+
// Indent each line of the body
|
|
1387
|
+
const bodyLines = body.split('\n');
|
|
1388
|
+
for (const line of bodyLines) {
|
|
1389
|
+
lines.push(` ${line}`);
|
|
1390
|
+
}
|
|
1391
|
+
lines.push(' }));');
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Watcher effects
|
|
1395
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
1396
|
+
const w = watchers[idx];
|
|
1397
|
+
const body = transformMethodBody(w.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1398
|
+
|
|
1399
|
+
if (w.kind === 'signal') {
|
|
1400
|
+
// Determine the signal reference for the watch target
|
|
1401
|
+
let watchRef;
|
|
1402
|
+
if (propNames.has(w.target)) {
|
|
1403
|
+
watchRef = `this._s_${w.target}()`;
|
|
1404
|
+
} else if (computedNames.includes(w.target)) {
|
|
1405
|
+
watchRef = `this._c_${w.target}()`;
|
|
1406
|
+
} else {
|
|
1407
|
+
watchRef = `this._${w.target}()`;
|
|
1408
|
+
}
|
|
1409
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1410
|
+
lines.push(` const ${w.newParam} = ${watchRef};`);
|
|
1411
|
+
lines.push(` if (this.__prev_${w.target} !== undefined && this.__prev_${w.target} !== ${w.newParam}) {`);
|
|
1412
|
+
if (w.oldParam) {
|
|
1413
|
+
lines.push(` const ${w.oldParam} = this.__prev_${w.target};`);
|
|
1414
|
+
}
|
|
1415
|
+
lines.push(' __untrack(() => {');
|
|
1416
|
+
const bodyLines = body.split('\n');
|
|
1417
|
+
for (const line of bodyLines) {
|
|
1418
|
+
lines.push(` ${line}`);
|
|
1419
|
+
}
|
|
1420
|
+
lines.push(' });');
|
|
1421
|
+
lines.push(' }');
|
|
1422
|
+
lines.push(` this.__prev_${w.target} = ${w.newParam};`);
|
|
1423
|
+
lines.push(' }));');
|
|
1424
|
+
} else {
|
|
1425
|
+
// kind === 'getter' — transform the getter expression and use it directly
|
|
1426
|
+
const getterExpr = transformMethodBody(w.target, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1427
|
+
const prevName = `__prev_watch${idx}`;
|
|
1428
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1429
|
+
lines.push(` const ${w.newParam} = ${getterExpr};`);
|
|
1430
|
+
lines.push(` if (this.${prevName} !== undefined && this.${prevName} !== ${w.newParam}) {`);
|
|
1431
|
+
if (w.oldParam) {
|
|
1432
|
+
lines.push(` const ${w.oldParam} = this.${prevName};`);
|
|
1433
|
+
}
|
|
1434
|
+
lines.push(' __untrack(() => {');
|
|
1435
|
+
const bodyLines2 = body.split('\n');
|
|
1436
|
+
for (const line of bodyLines2) {
|
|
1437
|
+
lines.push(` ${line}`);
|
|
1438
|
+
}
|
|
1439
|
+
lines.push(' });');
|
|
1440
|
+
lines.push(' }');
|
|
1441
|
+
lines.push(` this.${prevName} = ${w.newParam};`);
|
|
1442
|
+
lines.push(' }));');
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Event listeners (with AbortController signal for cleanup)
|
|
1447
|
+
if (events.length > 0) comment('Event listeners');
|
|
1448
|
+
for (const e of events) {
|
|
1449
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1450
|
+
lines.push(` this.${e.varName}.addEventListener('${e.event}', ${handlerExpr}, { signal: this.__ac.signal });`);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Show effects — one __effect per ShowBinding
|
|
1454
|
+
if (showBindings.length > 0) comment('Show directives');
|
|
1455
|
+
for (const sb of showBindings) {
|
|
1456
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1457
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1458
|
+
lines.push(` this.${sb.varName}.style.display = (${expr}) ? '' : 'none';`);
|
|
1459
|
+
lines.push(' }));');
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Model effects — signal → DOM (one __effect per ModelBinding)
|
|
1463
|
+
if (modelBindings.length > 0) comment('Model bindings (signal → DOM)');
|
|
1464
|
+
for (const mb of modelBindings) {
|
|
1465
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
1466
|
+
// Radio: compare signal value to radioValue
|
|
1467
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1468
|
+
lines.push(` this.${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
1469
|
+
lines.push(' }));');
|
|
1470
|
+
} else if (mb.prop === 'checked') {
|
|
1471
|
+
// Checkbox: coerce to boolean
|
|
1472
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1473
|
+
lines.push(` this.${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
1474
|
+
lines.push(' }));');
|
|
1475
|
+
} else {
|
|
1476
|
+
// Value-based (text, number, textarea, select): nullish coalesce to ''
|
|
1477
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1478
|
+
lines.push(` this.${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
1479
|
+
lines.push(' }));');
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Model event listeners — DOM → signal (with AbortController signal)
|
|
1484
|
+
for (const mb of modelBindings) {
|
|
1485
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
1486
|
+
// Checkbox: read e.target.checked
|
|
1487
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); }, { signal: this.__ac.signal });`);
|
|
1488
|
+
} else if (mb.coerce) {
|
|
1489
|
+
// Number input: wrap in Number()
|
|
1490
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); }, { signal: this.__ac.signal });`);
|
|
1491
|
+
} else {
|
|
1492
|
+
// All others: read e.target.value
|
|
1493
|
+
lines.push(` this.${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); }, { signal: this.__ac.signal });`);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// model:propName effects and listeners — bidirectional WCC-to-WCC binding
|
|
1498
|
+
for (const mpb of modelPropBindings) {
|
|
1499
|
+
// Determine the signal read/write expressions
|
|
1500
|
+
// If the signal is a model var, use this._m_propName(); otherwise use this._signalName()
|
|
1501
|
+
const isModelVar = modelVarMap.has(mpb.signal);
|
|
1502
|
+
const readExpr = isModelVar
|
|
1503
|
+
? `this._m_${modelVarMap.get(mpb.signal)}()`
|
|
1504
|
+
: `this._${mpb.signal}()`;
|
|
1505
|
+
const writeExpr = isModelVar
|
|
1506
|
+
? `this._m_${modelVarMap.get(mpb.signal)}`
|
|
1507
|
+
: `this._${mpb.signal}`;
|
|
1508
|
+
|
|
1509
|
+
// Reactive parent → child sync: set child's attribute from parent signal
|
|
1510
|
+
const attrName = camelToKebab(mpb.propName);
|
|
1511
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1512
|
+
lines.push(` this.${mpb.varName}.setAttribute('${attrName}', ${readExpr} ?? '');`);
|
|
1513
|
+
lines.push(' }));');
|
|
1514
|
+
|
|
1515
|
+
// Child → parent sync: listen for wcc:model on child, update parent signal
|
|
1516
|
+
lines.push(` this.${mpb.varName}.addEventListener('wcc:model', (e) => {`);
|
|
1517
|
+
lines.push(` if (e.detail.prop === '${mpb.propName}') {`);
|
|
1518
|
+
lines.push(` ${writeExpr}(e.detail.value);`);
|
|
1519
|
+
lines.push(' }');
|
|
1520
|
+
lines.push(' }, { signal: this.__ac.signal });');
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Attr binding effects — one __effect per AttrBinding
|
|
1524
|
+
for (const ab of attrBindings) {
|
|
1525
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1526
|
+
if (ab.kind === 'attr') {
|
|
1527
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1528
|
+
lines.push(` const __v = ${expr};`);
|
|
1529
|
+
lines.push(` if (__v || __v === '') { this.${ab.varName}.setAttribute('${ab.attr}', __v); }`);
|
|
1530
|
+
lines.push(` else { this.${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
1531
|
+
lines.push(' }));');
|
|
1532
|
+
} else if (ab.kind === 'bool') {
|
|
1533
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1534
|
+
lines.push(` this.${ab.varName}.${ab.attr} = !!(${expr});`);
|
|
1535
|
+
lines.push(' }));');
|
|
1536
|
+
} else if (ab.kind === 'class') {
|
|
1537
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
1538
|
+
// Object expression: iterate entries, classList.add/remove
|
|
1539
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1540
|
+
lines.push(` const __obj = ${expr};`);
|
|
1541
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
1542
|
+
lines.push(` __val ? this.${ab.varName}.classList.add(__k) : this.${ab.varName}.classList.remove(__k);`);
|
|
1543
|
+
lines.push(' }');
|
|
1544
|
+
lines.push(' }));');
|
|
1545
|
+
} else {
|
|
1546
|
+
// String expression: set className
|
|
1547
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1548
|
+
lines.push(` this.${ab.varName}.className = ${expr};`);
|
|
1549
|
+
lines.push(' }));');
|
|
1550
|
+
}
|
|
1551
|
+
} else if (ab.kind === 'style') {
|
|
1552
|
+
if (ab.expression.trimStart().startsWith('{')) {
|
|
1553
|
+
// Object expression: iterate entries, set style[key]
|
|
1554
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1555
|
+
lines.push(` const __obj = ${expr};`);
|
|
1556
|
+
lines.push(' for (const [__k, __val] of Object.entries(__obj)) {');
|
|
1557
|
+
lines.push(` this.${ab.varName}.style[__k] = __val;`);
|
|
1558
|
+
lines.push(' }');
|
|
1559
|
+
lines.push(' }));');
|
|
1560
|
+
} else {
|
|
1561
|
+
// String expression: set cssText
|
|
1562
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1563
|
+
lines.push(` this.${ab.varName}.style.cssText = ${expr};`);
|
|
1564
|
+
lines.push(' }));');
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// ── if effects ──
|
|
1570
|
+
for (const ifBlock of ifBlocks) {
|
|
1571
|
+
const vn = ifBlock.varName;
|
|
1572
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1573
|
+
lines.push(' let __branch = null;');
|
|
1574
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
1575
|
+
const branch = ifBlock.branches[i];
|
|
1576
|
+
if (branch.type === 'if') {
|
|
1577
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1578
|
+
lines.push(` if (${expr}) { __branch = ${i}; }`);
|
|
1579
|
+
} else if (branch.type === 'else-if') {
|
|
1580
|
+
const expr = transformExpr(branch.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1581
|
+
lines.push(` else if (${expr}) { __branch = ${i}; }`);
|
|
1582
|
+
} else {
|
|
1583
|
+
// else
|
|
1584
|
+
lines.push(` else { __branch = ${i}; }`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
lines.push(` if (__branch === this.${vn}_active) return;`);
|
|
1588
|
+
// Remove previous branch
|
|
1589
|
+
lines.push(` if (this.${vn}_current) { this.${vn}_current.remove(); this.${vn}_current = null; }`);
|
|
1590
|
+
// Insert new branch
|
|
1591
|
+
lines.push(' if (__branch !== null) {');
|
|
1592
|
+
const tplArray = ifBlock.branches.map((_, i) => `this.${vn}_t${i}`).join(', ');
|
|
1593
|
+
lines.push(` const tpl = [${tplArray}][__branch];`);
|
|
1594
|
+
lines.push(' const clone = tpl.content.cloneNode(true);');
|
|
1595
|
+
lines.push(' const node = clone.firstChild;');
|
|
1596
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
1597
|
+
lines.push(' customElements.upgrade(node);');
|
|
1598
|
+
lines.push(` this.${vn}_current = node;`);
|
|
1599
|
+
// Setup bindings/events for active branch (only if any branch has bindings/events)
|
|
1600
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
1601
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
1602
|
+
(b.events && b.events.length > 0) ||
|
|
1603
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
1604
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
1605
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
1606
|
+
);
|
|
1607
|
+
if (hasSetup) {
|
|
1608
|
+
lines.push(` this.${vn}_setup(node, __branch);`);
|
|
1609
|
+
}
|
|
1610
|
+
lines.push(' }');
|
|
1611
|
+
lines.push(` this.${vn}_active = __branch;`);
|
|
1612
|
+
lines.push(' }));');
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// ── each effects ──
|
|
1616
|
+
for (const forBlock of forBlocks) {
|
|
1617
|
+
const vn = forBlock.varName;
|
|
1618
|
+
const { itemVar, indexVar, source, keyExpr } = forBlock;
|
|
1619
|
+
|
|
1620
|
+
const signalNamesSet = new Set(signalNames);
|
|
1621
|
+
const computedNamesSet = new Set(computedNames);
|
|
1622
|
+
|
|
1623
|
+
// Transform the source expression
|
|
1624
|
+
const sourceExpr = transformForExpr(source, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
1625
|
+
|
|
1626
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1627
|
+
lines.push(` const __source = ${sourceExpr};`);
|
|
1628
|
+
lines.push('');
|
|
1629
|
+
lines.push(" const __iter = typeof __source === 'number'");
|
|
1630
|
+
lines.push(' ? Array.from({ length: __source }, (_, i) => i + 1)');
|
|
1631
|
+
lines.push(' : (__source || []);');
|
|
1632
|
+
lines.push('');
|
|
1633
|
+
|
|
1634
|
+
if (keyExpr) {
|
|
1635
|
+
// ── Keyed reconciliation ──
|
|
1636
|
+
lines.push(` const __oldMap = this.${vn}_keyMap || new Map();`);
|
|
1637
|
+
lines.push(' const __newMap = new Map();');
|
|
1638
|
+
lines.push(' const __newNodes = [];');
|
|
1639
|
+
lines.push('');
|
|
1640
|
+
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
1641
|
+
lines.push(` const __key = ${keyExpr};`);
|
|
1642
|
+
lines.push(' if (__oldMap.has(__key)) {');
|
|
1643
|
+
lines.push(' const node = __oldMap.get(__key);');
|
|
1644
|
+
lines.push(' __newMap.set(__key, node);');
|
|
1645
|
+
lines.push(' __newNodes.push(node);');
|
|
1646
|
+
lines.push(' __oldMap.delete(__key);');
|
|
1647
|
+
lines.push(' } else {');
|
|
1648
|
+
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
1649
|
+
lines.push(' const node = clone.firstChild;');
|
|
1650
|
+
|
|
1651
|
+
// Setup bindings/events/show/attr/model/slots for NEW nodes only
|
|
1652
|
+
// (reused nodes keep their existing bindings)
|
|
1653
|
+
generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
1654
|
+
|
|
1655
|
+
lines.push(' __newMap.set(__key, node);');
|
|
1656
|
+
lines.push(' __newNodes.push(node);');
|
|
1657
|
+
lines.push(' }');
|
|
1658
|
+
lines.push(' });');
|
|
1659
|
+
lines.push('');
|
|
1660
|
+
lines.push(' // Remove nodes no longer in the list');
|
|
1661
|
+
lines.push(' for (const n of __oldMap.values()) n.remove();');
|
|
1662
|
+
lines.push('');
|
|
1663
|
+
lines.push(' // Reorder: insert all nodes in correct order before anchor');
|
|
1664
|
+
lines.push(` for (const n of __newNodes) { this.${vn}_anchor.parentNode.insertBefore(n, this.${vn}_anchor); customElements.upgrade(n); }`);
|
|
1665
|
+
lines.push('');
|
|
1666
|
+
lines.push(` this.${vn}_nodes = __newNodes;`);
|
|
1667
|
+
lines.push(` this.${vn}_keyMap = __newMap;`);
|
|
1668
|
+
lines.push(' }));');
|
|
1669
|
+
} else {
|
|
1670
|
+
// ── Non-keyed: destroy all and recreate (original behavior) ──
|
|
1671
|
+
lines.push(` for (const n of this.${vn}_nodes) n.remove();`);
|
|
1672
|
+
lines.push(` this.${vn}_nodes = [];`);
|
|
1673
|
+
lines.push('');
|
|
1674
|
+
lines.push(` __iter.forEach((${itemVar}, ${indexVar || '__idx'}) => {`);
|
|
1675
|
+
lines.push(` const clone = this.${vn}_tpl.content.cloneNode(true);`);
|
|
1676
|
+
lines.push(' const node = clone.firstChild;');
|
|
1677
|
+
|
|
1678
|
+
generateItemSetup(lines, forBlock, itemVar, indexVar, propNames, signalNamesSet, computedNamesSet);
|
|
1679
|
+
|
|
1680
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(node, this.${vn}_anchor);`);
|
|
1681
|
+
lines.push(' customElements.upgrade(node);');
|
|
1682
|
+
lines.push(` this.${vn}_nodes.push(node);`);
|
|
1683
|
+
lines.push(' });');
|
|
1684
|
+
lines.push(' }));');
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ── dynamic component effects ──
|
|
1689
|
+
for (const dyn of dynamicComponents) {
|
|
1690
|
+
const vn = dyn.varName;
|
|
1691
|
+
const isExpr = transformExpr(dyn.isExpression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1692
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1693
|
+
lines.push(` const __tag = ${isExpr};`);
|
|
1694
|
+
lines.push(` if (__tag === this.${vn}_tag) return;`);
|
|
1695
|
+
lines.push(` if (this.${vn}_current) {`);
|
|
1696
|
+
lines.push(` this.${vn}_propDisposers.forEach(d => d());`);
|
|
1697
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1698
|
+
lines.push(` this.${vn}_current.remove();`);
|
|
1699
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1700
|
+
lines.push(' }');
|
|
1701
|
+
lines.push(' if (__tag) {');
|
|
1702
|
+
lines.push(' const el = document.createElement(__tag);');
|
|
1703
|
+
// Emit nested prop effects
|
|
1704
|
+
for (const prop of dyn.props) {
|
|
1705
|
+
const propExprTransformed = transformExpr(prop.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1706
|
+
lines.push(` this.${vn}_propDisposers.push(__effect(() => {`);
|
|
1707
|
+
lines.push(` el.setAttribute('${prop.attr}', ${propExprTransformed});`);
|
|
1708
|
+
lines.push(' }));');
|
|
1709
|
+
}
|
|
1710
|
+
// Emit event listeners
|
|
1711
|
+
for (const evt of dyn.events) {
|
|
1712
|
+
const handlerExpr = generateEventHandler(evt.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1713
|
+
lines.push(` el.addEventListener('${evt.event}', ${handlerExpr});`);
|
|
1714
|
+
}
|
|
1715
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(el, this.${vn}_anchor);`);
|
|
1716
|
+
lines.push(' customElements.upgrade(el);');
|
|
1717
|
+
lines.push(` this.${vn}_current = el;`);
|
|
1718
|
+
lines.push(' }');
|
|
1719
|
+
lines.push(` this.${vn}_tag = __tag;`);
|
|
1720
|
+
lines.push(' }));');
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
1724
|
+
for (const hook of onMountHooks) {
|
|
1725
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1726
|
+
if (hook.async) {
|
|
1727
|
+
lines.push(' ;(async () => {');
|
|
1728
|
+
const bodyLines = body.split('\n');
|
|
1729
|
+
for (const line of bodyLines) {
|
|
1730
|
+
lines.push(` ${line}`);
|
|
1731
|
+
}
|
|
1732
|
+
lines.push(' })();');
|
|
1733
|
+
} else {
|
|
1734
|
+
const bodyLines = body.split('\n');
|
|
1735
|
+
for (const line of bodyLines) {
|
|
1736
|
+
const trimmed = line.trimEnd();
|
|
1737
|
+
const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
|
|
1738
|
+
lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Close connectedCallback
|
|
1744
|
+
lines.push(' }');
|
|
1745
|
+
lines.push('');
|
|
1746
|
+
|
|
1747
|
+
// disconnectedCallback (cleanup: abort listeners + dispose effects + user hooks)
|
|
1748
|
+
lines.push(' disconnectedCallback() {');
|
|
1749
|
+
lines.push(' this.__connected = false;');
|
|
1750
|
+
lines.push(' this.__ac.abort();');
|
|
1751
|
+
lines.push(' this.__disposers.forEach(d => d());');
|
|
1752
|
+
if (onDestroyHooks.length > 0) {
|
|
1753
|
+
for (const hook of onDestroyHooks) {
|
|
1754
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1755
|
+
if (hook.async) {
|
|
1756
|
+
lines.push(' ;(async () => {');
|
|
1757
|
+
const bodyLines = body.split('\n');
|
|
1758
|
+
for (const line of bodyLines) {
|
|
1759
|
+
lines.push(` ${line}`);
|
|
1760
|
+
}
|
|
1761
|
+
lines.push(' })();');
|
|
1762
|
+
} else {
|
|
1763
|
+
const bodyLines = body.split('\n');
|
|
1764
|
+
for (const line of bodyLines) {
|
|
1765
|
+
const trimmed = line.trimEnd();
|
|
1766
|
+
const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
|
|
1767
|
+
lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
lines.push(' }');
|
|
1773
|
+
lines.push('');
|
|
1774
|
+
|
|
1775
|
+
// adoptedCallback (if onAdopt hooks exist)
|
|
1776
|
+
if (onAdoptHooks.length > 0) {
|
|
1777
|
+
lines.push(' adoptedCallback() {');
|
|
1778
|
+
for (const hook of onAdoptHooks) {
|
|
1779
|
+
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1780
|
+
if (hook.async) {
|
|
1781
|
+
lines.push(' ;(async () => {');
|
|
1782
|
+
const bodyLines = body.split('\n');
|
|
1783
|
+
for (const line of bodyLines) {
|
|
1784
|
+
lines.push(` ${line}`);
|
|
1785
|
+
}
|
|
1786
|
+
lines.push(' })();');
|
|
1787
|
+
} else {
|
|
1788
|
+
const bodyLines = body.split('\n');
|
|
1789
|
+
for (const line of bodyLines) {
|
|
1790
|
+
const trimmed = line.trimEnd();
|
|
1791
|
+
const needsSemi = trimmed && !trimmed.endsWith(';') && !trimmed.endsWith('{') && !trimmed.endsWith('}');
|
|
1792
|
+
lines.push(` ${trimmed}${needsSemi ? ';' : ''}`);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
lines.push(' }');
|
|
1797
|
+
lines.push('');
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// attributeChangedCallback (if props or model props exist)
|
|
1801
|
+
if (propDefs.length > 0 || modelDefs.length > 0) {
|
|
1802
|
+
lines.push(' attributeChangedCallback(name, oldVal, newVal) {');
|
|
1803
|
+
for (const p of propDefs) {
|
|
1804
|
+
const defaultVal = p.default;
|
|
1805
|
+
let updateExpr;
|
|
1806
|
+
|
|
1807
|
+
if (defaultVal === 'true' || defaultVal === 'false') {
|
|
1808
|
+
// Boolean coercion: attribute presence = true
|
|
1809
|
+
updateExpr = `this._s_${p.name}(newVal != null)`;
|
|
1810
|
+
} else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
|
|
1811
|
+
// Number coercion
|
|
1812
|
+
updateExpr = `this._s_${p.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
|
|
1813
|
+
} else if (defaultVal === 'undefined') {
|
|
1814
|
+
// Undefined default — pass through
|
|
1815
|
+
updateExpr = `this._s_${p.name}(newVal)`;
|
|
1816
|
+
} else {
|
|
1817
|
+
// String default — use nullish coalescing
|
|
1818
|
+
updateExpr = `this._s_${p.name}(newVal ?? ${defaultVal})`;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
lines.push(` if (name === '${p.attrName}') ${updateExpr};`);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// Model props — update signal directly (NO event emission)
|
|
1825
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
1826
|
+
const md = modelDefs[i];
|
|
1827
|
+
const attrName = modelAttrNames[i];
|
|
1828
|
+
const camelName = md.name;
|
|
1829
|
+
const defaultVal = md.default;
|
|
1830
|
+
let updateExpr;
|
|
1831
|
+
|
|
1832
|
+
if (defaultVal === 'true' || defaultVal === 'false') {
|
|
1833
|
+
// Boolean coercion: attribute presence = true
|
|
1834
|
+
updateExpr = `this._m_${md.name}(newVal != null)`;
|
|
1835
|
+
} else if (/^-?\d+(\.\d+)?$/.test(defaultVal)) {
|
|
1836
|
+
// Number coercion
|
|
1837
|
+
updateExpr = `this._m_${md.name}(newVal != null ? Number(newVal) : ${defaultVal})`;
|
|
1838
|
+
} else if (defaultVal === 'undefined') {
|
|
1839
|
+
// Undefined default — pass through
|
|
1840
|
+
updateExpr = `this._m_${md.name}(newVal)`;
|
|
1841
|
+
} else {
|
|
1842
|
+
// String default — use nullish coalescing
|
|
1843
|
+
updateExpr = `this._m_${md.name}(newVal ?? ${defaultVal})`;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Handle both kebab-case (native HTML) and camelCase (Vue) attribute names
|
|
1847
|
+
if (attrName !== camelName) {
|
|
1848
|
+
lines.push(` if (name === '${attrName}' || name === '${camelName}') ${updateExpr};`);
|
|
1849
|
+
} else {
|
|
1850
|
+
lines.push(` if (name === '${attrName}') ${updateExpr};`);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
lines.push(' }');
|
|
1855
|
+
lines.push('');
|
|
1856
|
+
|
|
1857
|
+
// Public getters and setters
|
|
1858
|
+
for (const p of propDefs) {
|
|
1859
|
+
lines.push(` get ${p.name}() { return this._s_${p.name}(); }`);
|
|
1860
|
+
lines.push(` set ${p.name}(val) { this._s_${p.name}(val); this.setAttribute('${p.attrName}', String(val)); }`);
|
|
1861
|
+
lines.push('');
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// Public getters and setters for model props
|
|
1865
|
+
for (let i = 0; i < modelDefs.length; i++) {
|
|
1866
|
+
const md = modelDefs[i];
|
|
1867
|
+
const attrName = modelAttrNames[i];
|
|
1868
|
+
lines.push(` get ${md.name}() { return this._m_${md.name}(); }`);
|
|
1869
|
+
lines.push(` set ${md.name}(val) { this._m_${md.name}(val); this.setAttribute('${attrName}', String(val)); }`);
|
|
1870
|
+
lines.push('');
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// _emit method (if emits declared)
|
|
1875
|
+
// Emits the original event name + lowercase-no-hyphens for React 19 compatibility.
|
|
1876
|
+
// React 19 maps `oncountchanged` → addEventListener('countchanged').
|
|
1877
|
+
if (emits.length > 0) {
|
|
1878
|
+
lines.push(' _emit(name, detail) {');
|
|
1879
|
+
lines.push(' const evt = { detail, bubbles: true, composed: true };');
|
|
1880
|
+
lines.push(' this.dispatchEvent(new CustomEvent(name, evt));');
|
|
1881
|
+
lines.push(" const lower = name.replace(/-/g, '').toLowerCase();");
|
|
1882
|
+
lines.push(' if (lower !== name) this.dispatchEvent(new CustomEvent(lower, evt));');
|
|
1883
|
+
lines.push(' }');
|
|
1884
|
+
lines.push('');
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// _modelSet methods (one per defineModel prop — emits events on internal write)
|
|
1888
|
+
// Emits:
|
|
1889
|
+
// 1. wcc:model — canonical event for vanilla JS, WCC-to-WCC, React adapter, Vue plugin
|
|
1890
|
+
// 2. propNameChange — for Angular [(prop)] banana-box syntax (zero-config)
|
|
1891
|
+
for (const md of modelDefs) {
|
|
1892
|
+
lines.push(` _modelSet_${md.name}(newVal) {`);
|
|
1893
|
+
lines.push(` const oldVal = this._m_${md.name}();`);
|
|
1894
|
+
lines.push(` this._m_${md.name}(newVal);`);
|
|
1895
|
+
lines.push(` this.dispatchEvent(new CustomEvent('wcc:model', {`);
|
|
1896
|
+
lines.push(` detail: { prop: '${md.name}', value: newVal, oldValue: oldVal },`);
|
|
1897
|
+
lines.push(` bubbles: true,`);
|
|
1898
|
+
lines.push(` composed: true`);
|
|
1899
|
+
lines.push(` }));`);
|
|
1900
|
+
lines.push(` this.dispatchEvent(new CustomEvent('${md.name}Change', { detail: newVal, bubbles: true }));`);
|
|
1901
|
+
lines.push(' }');
|
|
1902
|
+
lines.push('');
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// __scopedSlots instance getter and registerSlotRenderer (if scoped slots exist)
|
|
1906
|
+
if (scopedSlotNames.length > 0) {
|
|
1907
|
+
lines.push(' get __scopedSlots() { return this.constructor.__scopedSlots || []; }');
|
|
1908
|
+
lines.push('');
|
|
1909
|
+
lines.push(' registerSlotRenderer(slotName, callback) {');
|
|
1910
|
+
lines.push(' if (!this.__slotRenderers) this.__slotRenderers = {};');
|
|
1911
|
+
lines.push(' this.__slotRenderers[slotName] = callback;');
|
|
1912
|
+
lines.push(' if (this.__slotProps && this.__slotProps[slotName]) {');
|
|
1913
|
+
lines.push(' callback(this.__slotProps[slotName]);');
|
|
1914
|
+
lines.push(' }');
|
|
1915
|
+
lines.push(' return () => {');
|
|
1916
|
+
lines.push(' if (this.__slotRenderers) {');
|
|
1917
|
+
lines.push(' delete this.__slotRenderers[slotName];');
|
|
1918
|
+
lines.push(' }');
|
|
1919
|
+
lines.push(' };');
|
|
1920
|
+
lines.push(' }');
|
|
1921
|
+
lines.push('');
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// User methods (prefixed with _)
|
|
1925
|
+
if (methods.length > 0 && options.comments) lines.push('');
|
|
1926
|
+
if (methods.length > 0 && options.comments) lines.push(' // --- Methods ---');
|
|
1927
|
+
for (const m of methods) {
|
|
1928
|
+
const body = transformMethodBody(m.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
|
1929
|
+
lines.push(` _${m.name}(${m.params}) {`);
|
|
1930
|
+
const bodyLines = body.split('\n');
|
|
1931
|
+
for (const line of bodyLines) {
|
|
1932
|
+
lines.push(` ${line}`);
|
|
1933
|
+
}
|
|
1934
|
+
lines.push(' }');
|
|
1935
|
+
lines.push('');
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// ── Ref getter properties ──
|
|
1939
|
+
for (const rd of refs) {
|
|
1940
|
+
// Find matching RefBinding
|
|
1941
|
+
const rb = refBindings.find(b => b.refName === rd.refName);
|
|
1942
|
+
if (rb) {
|
|
1943
|
+
lines.push(` get _${rd.varName}() { return { value: this._ref_${rd.refName} }; }`);
|
|
1944
|
+
lines.push('');
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// ── defineExpose: public getters/methods ──
|
|
1949
|
+
for (const name of exposeNames) {
|
|
1950
|
+
if (computedNames.includes(name)) {
|
|
1951
|
+
lines.push(` get ${name}() { return this._c_${name}(); }`);
|
|
1952
|
+
} else if (signalNames.includes(name)) {
|
|
1953
|
+
lines.push(` get ${name}() { return this._${name}(); }`);
|
|
1954
|
+
} else if (methodNames.includes(name)) {
|
|
1955
|
+
lines.push(` ${name}(...args) { return this._${name}(...args); }`);
|
|
1956
|
+
} else if (constantNames.includes(name)) {
|
|
1957
|
+
lines.push(` get ${name}() { return this._const_${name}; }`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
if (exposeNames.length > 0) lines.push('');
|
|
1961
|
+
|
|
1962
|
+
// ── if setup methods ──
|
|
1963
|
+
for (const ifBlock of ifBlocks) {
|
|
1964
|
+
const vn = ifBlock.varName;
|
|
1965
|
+
const hasSetup = ifBlock.branches.some(b =>
|
|
1966
|
+
(b.bindings && b.bindings.length > 0) ||
|
|
1967
|
+
(b.events && b.events.length > 0) ||
|
|
1968
|
+
(b.showBindings && b.showBindings.length > 0) ||
|
|
1969
|
+
(b.attrBindings && b.attrBindings.length > 0) ||
|
|
1970
|
+
(b.modelBindings && b.modelBindings.length > 0)
|
|
1971
|
+
);
|
|
1972
|
+
if (!hasSetup) continue;
|
|
1973
|
+
|
|
1974
|
+
lines.push(` ${vn}_setup(node, branch) {`);
|
|
1975
|
+
for (let i = 0; i < ifBlock.branches.length; i++) {
|
|
1976
|
+
const branch = ifBlock.branches[i];
|
|
1977
|
+
const hasBranchSetup =
|
|
1978
|
+
(branch.bindings && branch.bindings.length > 0) ||
|
|
1979
|
+
(branch.events && branch.events.length > 0) ||
|
|
1980
|
+
(branch.showBindings && branch.showBindings.length > 0) ||
|
|
1981
|
+
(branch.attrBindings && branch.attrBindings.length > 0) ||
|
|
1982
|
+
(branch.modelBindings && branch.modelBindings.length > 0);
|
|
1983
|
+
if (!hasBranchSetup) continue;
|
|
1984
|
+
|
|
1985
|
+
const keyword = i === 0 ? 'if' : 'else if';
|
|
1986
|
+
lines.push(` ${keyword} (branch === ${i}) {`);
|
|
1987
|
+
|
|
1988
|
+
// Bindings: generate DOM refs and effects for text bindings
|
|
1989
|
+
for (const b of branch.bindings) {
|
|
1990
|
+
lines.push(` const ${b.varName} = ${pathExpr(b.path, 'node')};`);
|
|
1991
|
+
if (b.type === 'prop') {
|
|
1992
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._s_${b.name}() ?? ''; });`);
|
|
1993
|
+
} else if (b.type === 'signal') {
|
|
1994
|
+
const modelPropName = modelVarMap.get(b.name);
|
|
1995
|
+
const signalRef = modelPropName ? `this._m_${modelPropName}()` : `this._${b.name}()`;
|
|
1996
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = ${signalRef} ?? ''; });`);
|
|
1997
|
+
} else if (b.type === 'computed') {
|
|
1998
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = this._c_${b.name}() ?? ''; });`);
|
|
1999
|
+
} else {
|
|
2000
|
+
// method/expression type — check for props.x pattern
|
|
2001
|
+
let ref;
|
|
2002
|
+
if (propsObjectName && b.name.startsWith(propsObjectName + '.')) {
|
|
2003
|
+
const propName = b.name.slice(propsObjectName.length + 1);
|
|
2004
|
+
ref = `this._s_${propName}()`;
|
|
2005
|
+
} else {
|
|
2006
|
+
ref = transformExpr(b.name, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
2007
|
+
}
|
|
2008
|
+
lines.push(` __effect(() => { ${b.varName}.textContent = ${ref} ?? ''; });`);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Events: generate addEventListener
|
|
2013
|
+
for (const e of branch.events) {
|
|
2014
|
+
const handlerExpr = generateEventHandler(e.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
2015
|
+
lines.push(` const ${e.varName} = ${pathExpr(e.path, 'node')};`);
|
|
2016
|
+
lines.push(` ${e.varName}.addEventListener('${e.event}', ${handlerExpr});`);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// Show bindings: generate effects
|
|
2020
|
+
for (const sb of branch.showBindings) {
|
|
2021
|
+
const expr = transformExpr(sb.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
2022
|
+
lines.push(` const ${sb.varName} = ${pathExpr(sb.path, 'node')};`);
|
|
2023
|
+
lines.push(` __effect(() => { ${sb.varName}.style.display = (${expr}) ? '' : 'none'; });`);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Attr bindings: generate effects
|
|
2027
|
+
for (const ab of branch.attrBindings) {
|
|
2028
|
+
const expr = transformExpr(ab.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
2029
|
+
lines.push(` const ${ab.varName} = ${pathExpr(ab.path, 'node')};`);
|
|
2030
|
+
lines.push(` __effect(() => {`);
|
|
2031
|
+
lines.push(` const __val = ${expr};`);
|
|
2032
|
+
lines.push(` if (__val == null || __val === false) { ${ab.varName}.removeAttribute('${ab.attr}'); }`);
|
|
2033
|
+
lines.push(` else { ${ab.varName}.setAttribute('${ab.attr}', __val); }`);
|
|
2034
|
+
lines.push(` });`);
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Model bindings: generate effects and listeners
|
|
2038
|
+
for (const mb of (branch.modelBindings || [])) {
|
|
2039
|
+
const nodeRef = pathExpr(mb.path, 'node');
|
|
2040
|
+
lines.push(` const ${mb.varName} = ${nodeRef};`);
|
|
2041
|
+
// Effect (signal → DOM)
|
|
2042
|
+
lines.push(` __effect(() => {`);
|
|
2043
|
+
if (mb.prop === 'checked' && mb.radioValue !== null) {
|
|
2044
|
+
lines.push(` ${mb.varName}.checked = (this._${mb.signal}() === '${mb.radioValue}');`);
|
|
2045
|
+
} else if (mb.prop === 'checked') {
|
|
2046
|
+
lines.push(` ${mb.varName}.checked = !!this._${mb.signal}();`);
|
|
2047
|
+
} else {
|
|
2048
|
+
lines.push(` ${mb.varName}.value = this._${mb.signal}() ?? '';`);
|
|
2049
|
+
}
|
|
2050
|
+
lines.push(` });`);
|
|
2051
|
+
// Listener (DOM → signal)
|
|
2052
|
+
if (mb.prop === 'checked' && mb.radioValue === null) {
|
|
2053
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.checked); });`);
|
|
2054
|
+
} else if (mb.coerce) {
|
|
2055
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(Number(e.target.value)); });`);
|
|
2056
|
+
} else {
|
|
2057
|
+
lines.push(` ${mb.varName}.addEventListener('${mb.event}', (e) => { this._${mb.signal}(e.target.value); });`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
lines.push(' }');
|
|
2062
|
+
}
|
|
2063
|
+
lines.push(' }');
|
|
2064
|
+
lines.push('');
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
lines.push('}');
|
|
2068
|
+
lines.push('');
|
|
2069
|
+
|
|
2070
|
+
// ── 5. Custom element registration ──
|
|
2071
|
+
lines.push(`if (!customElements.get('${tagName}')) customElements.define('${tagName}', ${className});`);
|
|
2072
|
+
lines.push('');
|
|
2073
|
+
|
|
2074
|
+
// ── 6. Default export (enables named imports from parent components) ──
|
|
2075
|
+
lines.push(`export default ${className};`);
|
|
2076
|
+
|
|
2077
|
+
return lines.join('\n');
|
|
2078
|
+
}
|