what-compiler 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/babel-plugin.js +778 -152
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "JSX compiler for What Framework - transforms JSX to optimized DOM operations",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@babel/core": "^7.0.0",
|
|
25
|
-
"what-core": "^0.
|
|
25
|
+
"what-core": "^0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src"
|
package/src/babel-plugin.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What Framework Babel Plugin
|
|
3
|
-
* Transforms JSX into h() calls, routing all rendering through core's VNode reconciler.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - 'vdom' (legacy): JSX → h() calls through VNode reconciler
|
|
6
|
+
* - 'fine-grained' (default): JSX → template() + insert() + effect() calls
|
|
7
|
+
* Static HTML extracted to templates, dynamic expressions wrapped in effects.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* Fine-grained output:
|
|
10
|
+
* const _t$ = template('<div class="container"><h1>Title</h1><p></p></div>');
|
|
11
|
+
* function App() {
|
|
12
|
+
* const _el$ = _t$();
|
|
13
|
+
* insert(_el$.children[1], () => desc());
|
|
14
|
+
* return _el$;
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* VDOM output (legacy):
|
|
18
|
+
* h('div', { class: 'container' }, h('h1', null, 'Title'), h('p', null, desc()))
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
const EVENT_MODIFIERS = new Set(['preventDefault', 'stopPropagation', 'once', 'capture', 'passive', 'self']);
|
|
20
22
|
const EVENT_OPTION_MODIFIERS = new Set(['once', 'capture', 'passive']);
|
|
21
23
|
|
|
22
24
|
export default function whatBabelPlugin({ types: t }) {
|
|
25
|
+
const mode = 'fine-grained'; // Can be overridden via plugin options
|
|
26
|
+
|
|
27
|
+
// =====================================================
|
|
28
|
+
// Shared utilities (used by both modes)
|
|
29
|
+
// =====================================================
|
|
23
30
|
|
|
24
|
-
// Parse event modifiers from attribute name like onClick|preventDefault|once
|
|
25
31
|
function parseEventModifiers(name) {
|
|
26
32
|
const parts = name.split('|');
|
|
27
33
|
const eventName = parts[0];
|
|
@@ -29,22 +35,18 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
29
35
|
return { eventName, modifiers };
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
// Check if attribute is a binding
|
|
33
38
|
function isBindingAttribute(name) {
|
|
34
39
|
return name.startsWith('bind:');
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
// Get the binding property from bind:value -> value
|
|
38
42
|
function getBindingProperty(name) {
|
|
39
43
|
return name.slice(5);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
// Check if element is a component (starts with uppercase)
|
|
43
46
|
function isComponent(name) {
|
|
44
47
|
return /^[A-Z]/.test(name);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
// Get the expression from a JSX attribute value
|
|
48
50
|
function getAttributeValue(value) {
|
|
49
51
|
if (!value) return t.booleanLiteral(true);
|
|
50
52
|
if (t.isJSXExpressionContainer(value)) return value.expression;
|
|
@@ -52,7 +54,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
52
54
|
return t.stringLiteral(value.value || '');
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
// Create event handler wrapper for inline modifiers (preventDefault, stopPropagation, self)
|
|
56
57
|
function createEventHandler(handler, modifiers) {
|
|
57
58
|
if (modifiers.length === 0) return handler;
|
|
58
59
|
|
|
@@ -112,7 +113,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
112
113
|
);
|
|
113
114
|
break;
|
|
114
115
|
|
|
115
|
-
// once, capture, passive are handled via _eventOpts, not handler wrapping
|
|
116
116
|
case 'once':
|
|
117
117
|
case 'capture':
|
|
118
118
|
case 'passive':
|
|
@@ -123,52 +123,30 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
123
123
|
return wrappedHandler;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (optionMods.length === 0) return [];
|
|
130
|
-
|
|
131
|
-
const optsProps = optionMods.map(m =>
|
|
132
|
-
t.objectProperty(t.identifier(m), t.booleanLiteral(true))
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// handler._eventOpts = { once: true, ... }
|
|
136
|
-
return [
|
|
137
|
-
t.expressionStatement(
|
|
138
|
-
t.assignmentExpression(
|
|
139
|
-
'=',
|
|
140
|
-
t.memberExpression(handlerIdentifier, t.identifier('_eventOpts')),
|
|
141
|
-
t.objectExpression(optsProps)
|
|
142
|
-
)
|
|
143
|
-
)
|
|
144
|
-
];
|
|
145
|
-
}
|
|
126
|
+
// =====================================================
|
|
127
|
+
// VDOM Mode (legacy h() calls)
|
|
128
|
+
// =====================================================
|
|
146
129
|
|
|
147
|
-
|
|
148
|
-
function transformChildren(children, state) {
|
|
130
|
+
function transformChildrenVdom(children, state) {
|
|
149
131
|
const result = [];
|
|
150
132
|
for (const child of children) {
|
|
151
133
|
if (t.isJSXText(child)) {
|
|
152
134
|
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
153
|
-
if (text)
|
|
154
|
-
result.push(t.stringLiteral(text));
|
|
155
|
-
}
|
|
135
|
+
if (text) result.push(t.stringLiteral(text));
|
|
156
136
|
} else if (t.isJSXExpressionContainer(child)) {
|
|
157
137
|
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
158
138
|
result.push(child.expression);
|
|
159
139
|
}
|
|
160
140
|
} else if (t.isJSXElement(child)) {
|
|
161
|
-
result.push(
|
|
141
|
+
result.push(transformElementVdom({ node: child }, state));
|
|
162
142
|
} else if (t.isJSXFragment(child)) {
|
|
163
|
-
|
|
164
|
-
result.push(transformFragment({ node: child }, state));
|
|
143
|
+
result.push(transformFragmentVdom({ node: child }, state));
|
|
165
144
|
}
|
|
166
145
|
}
|
|
167
146
|
return result;
|
|
168
147
|
}
|
|
169
148
|
|
|
170
|
-
|
|
171
|
-
function transformElement(path, state) {
|
|
149
|
+
function transformElementVdom(path, state) {
|
|
172
150
|
const { node } = path;
|
|
173
151
|
const openingElement = node.openingElement;
|
|
174
152
|
const tagName = openingElement.name.name;
|
|
@@ -176,14 +154,12 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
176
154
|
const children = node.children;
|
|
177
155
|
|
|
178
156
|
if (isComponent(tagName)) {
|
|
179
|
-
return
|
|
157
|
+
return transformComponentVdom(path, state);
|
|
180
158
|
}
|
|
181
159
|
|
|
182
|
-
// Build props
|
|
183
160
|
const props = [];
|
|
184
161
|
let hasSpread = false;
|
|
185
162
|
let spreadExpr = null;
|
|
186
|
-
const eventOptsStatements = [];
|
|
187
163
|
|
|
188
164
|
for (const attr of attributes) {
|
|
189
165
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
@@ -194,21 +170,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
194
170
|
|
|
195
171
|
const attrName = typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
|
|
196
172
|
|
|
197
|
-
// Handle event modifiers: onClick|preventDefault
|
|
198
173
|
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
199
174
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
200
175
|
const handler = getAttributeValue(attr.value);
|
|
201
176
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
202
177
|
|
|
203
|
-
// Check if we need _eventOpts (once/capture/passive)
|
|
204
178
|
const optionMods = modifiers.filter(m => EVENT_OPTION_MODIFIERS.has(m));
|
|
205
179
|
if (optionMods.length > 0) {
|
|
206
|
-
// Need a temp variable for the handler to attach _eventOpts
|
|
207
180
|
const tempId = path.scope
|
|
208
181
|
? path.scope.generateUidIdentifier('handler')
|
|
209
182
|
: t.identifier('_h' + Math.random().toString(36).slice(2, 6));
|
|
210
183
|
|
|
211
|
-
// We'll use an IIFE: (() => { const _h = handler; _h._eventOpts = {...}; return _h; })()
|
|
212
184
|
const optsProps = optionMods.map(m =>
|
|
213
185
|
t.objectProperty(t.identifier(m), t.booleanLiteral(true))
|
|
214
186
|
);
|
|
@@ -233,29 +205,20 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
233
205
|
[]
|
|
234
206
|
);
|
|
235
207
|
|
|
236
|
-
props.push(
|
|
237
|
-
t.objectProperty(t.identifier(eventName), iifeHandler)
|
|
238
|
-
);
|
|
208
|
+
props.push(t.objectProperty(t.identifier(eventName), iifeHandler));
|
|
239
209
|
} else {
|
|
240
|
-
props.push(
|
|
241
|
-
t.objectProperty(t.identifier(eventName), wrappedHandler)
|
|
242
|
-
);
|
|
210
|
+
props.push(t.objectProperty(t.identifier(eventName), wrappedHandler));
|
|
243
211
|
}
|
|
244
212
|
continue;
|
|
245
213
|
}
|
|
246
214
|
|
|
247
|
-
// Handle two-way binding: bind:value={sig}
|
|
248
215
|
if (isBindingAttribute(attrName)) {
|
|
249
216
|
const bindProp = getBindingProperty(attrName);
|
|
250
217
|
const signalExpr = attr.value.expression;
|
|
251
218
|
|
|
252
219
|
if (bindProp === 'value') {
|
|
253
|
-
// { value: sig(), onInput: (e) => sig.set(e.target.value) }
|
|
254
220
|
props.push(
|
|
255
|
-
t.objectProperty(
|
|
256
|
-
t.identifier('value'),
|
|
257
|
-
t.callExpression(t.cloneNode(signalExpr), [])
|
|
258
|
-
)
|
|
221
|
+
t.objectProperty(t.identifier('value'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
259
222
|
);
|
|
260
223
|
props.push(
|
|
261
224
|
t.objectProperty(
|
|
@@ -273,12 +236,8 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
273
236
|
)
|
|
274
237
|
);
|
|
275
238
|
} else if (bindProp === 'checked') {
|
|
276
|
-
// { checked: sig(), onChange: (e) => sig.set(e.target.checked) }
|
|
277
239
|
props.push(
|
|
278
|
-
t.objectProperty(
|
|
279
|
-
t.identifier('checked'),
|
|
280
|
-
t.callExpression(t.cloneNode(signalExpr), [])
|
|
281
|
-
)
|
|
240
|
+
t.objectProperty(t.identifier('checked'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
282
241
|
);
|
|
283
242
|
props.push(
|
|
284
243
|
t.objectProperty(
|
|
@@ -299,17 +258,13 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
299
258
|
continue;
|
|
300
259
|
}
|
|
301
260
|
|
|
302
|
-
// Regular attributes
|
|
303
261
|
const value = getAttributeValue(attr.value);
|
|
304
|
-
|
|
305
|
-
// Normalize className -> class, htmlFor -> for
|
|
306
262
|
let domAttrName = attrName;
|
|
307
263
|
if (attrName === 'className') domAttrName = 'class';
|
|
308
264
|
if (attrName === 'htmlFor') domAttrName = 'for';
|
|
309
265
|
|
|
310
266
|
props.push(
|
|
311
267
|
t.objectProperty(
|
|
312
|
-
// Use identifier for valid JS identifiers, string literal otherwise
|
|
313
268
|
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(domAttrName)
|
|
314
269
|
? t.identifier(domAttrName)
|
|
315
270
|
: t.stringLiteral(domAttrName),
|
|
@@ -318,8 +273,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
318
273
|
);
|
|
319
274
|
}
|
|
320
275
|
|
|
321
|
-
|
|
322
|
-
const transformedChildren = transformChildren(children, state);
|
|
276
|
+
const transformedChildren = transformChildrenVdom(children, state);
|
|
323
277
|
|
|
324
278
|
let propsExpr;
|
|
325
279
|
if (hasSpread) {
|
|
@@ -338,20 +292,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
338
292
|
}
|
|
339
293
|
|
|
340
294
|
const args = [t.stringLiteral(tagName), propsExpr, ...transformedChildren];
|
|
341
|
-
|
|
342
295
|
state.needsH = true;
|
|
343
296
|
return t.callExpression(t.identifier('h'), args);
|
|
344
297
|
}
|
|
345
298
|
|
|
346
|
-
|
|
347
|
-
function transformComponent(path, state) {
|
|
299
|
+
function transformComponentVdom(path, state) {
|
|
348
300
|
const { node } = path;
|
|
349
301
|
const openingElement = node.openingElement;
|
|
350
302
|
const componentName = openingElement.name.name;
|
|
351
303
|
const attributes = openingElement.attributes;
|
|
352
304
|
const children = node.children;
|
|
353
305
|
|
|
354
|
-
// Check for client directives (islands)
|
|
355
306
|
let clientDirective = null;
|
|
356
307
|
const filteredAttrs = [];
|
|
357
308
|
|
|
@@ -359,7 +310,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
359
310
|
if (t.isJSXAttribute(attr)) {
|
|
360
311
|
const name = attr.name.name;
|
|
361
312
|
if (name && name.startsWith('client:')) {
|
|
362
|
-
const mode = name.slice(7);
|
|
313
|
+
const mode = name.slice(7);
|
|
363
314
|
if (attr.value) {
|
|
364
315
|
clientDirective = { type: mode, value: attr.value.value };
|
|
365
316
|
} else {
|
|
@@ -371,29 +322,21 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
371
322
|
filteredAttrs.push(attr);
|
|
372
323
|
}
|
|
373
324
|
|
|
374
|
-
// Handle islands — h(Island, { component: Comp, mode: 'idle', ...props })
|
|
375
325
|
if (clientDirective) {
|
|
376
326
|
state.needsH = true;
|
|
377
327
|
state.needsIsland = true;
|
|
378
328
|
|
|
379
329
|
const islandProps = [
|
|
380
330
|
t.objectProperty(t.identifier('component'), t.identifier(componentName)),
|
|
381
|
-
t.objectProperty(
|
|
382
|
-
t.identifier('mode'),
|
|
383
|
-
t.stringLiteral(clientDirective.type)
|
|
384
|
-
),
|
|
331
|
+
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
385
332
|
];
|
|
386
333
|
|
|
387
334
|
if (clientDirective.value) {
|
|
388
335
|
islandProps.push(
|
|
389
|
-
t.objectProperty(
|
|
390
|
-
t.identifier('mediaQuery'),
|
|
391
|
-
t.stringLiteral(clientDirective.value)
|
|
392
|
-
)
|
|
336
|
+
t.objectProperty(t.identifier('mediaQuery'), t.stringLiteral(clientDirective.value))
|
|
393
337
|
);
|
|
394
338
|
}
|
|
395
339
|
|
|
396
|
-
// Add remaining props
|
|
397
340
|
for (const attr of filteredAttrs) {
|
|
398
341
|
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
399
342
|
const attrName = attr.name.name;
|
|
@@ -407,7 +350,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
407
350
|
);
|
|
408
351
|
}
|
|
409
352
|
|
|
410
|
-
// Build props
|
|
411
353
|
const props = [];
|
|
412
354
|
let hasSpread = false;
|
|
413
355
|
let spreadExpr = null;
|
|
@@ -432,10 +374,8 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
432
374
|
);
|
|
433
375
|
}
|
|
434
376
|
|
|
435
|
-
|
|
436
|
-
const transformedChildren = transformChildren(children, state);
|
|
377
|
+
const transformedChildren = transformChildrenVdom(children, state);
|
|
437
378
|
|
|
438
|
-
// Build props expression
|
|
439
379
|
let propsExpr;
|
|
440
380
|
if (hasSpread) {
|
|
441
381
|
if (props.length > 0) {
|
|
@@ -452,17 +392,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
452
392
|
propsExpr = t.nullLiteral();
|
|
453
393
|
}
|
|
454
394
|
|
|
455
|
-
// h(Component, props, ...children)
|
|
456
395
|
const args = [t.identifier(componentName), propsExpr, ...transformedChildren];
|
|
457
|
-
|
|
458
396
|
state.needsH = true;
|
|
459
397
|
return t.callExpression(t.identifier('h'), args);
|
|
460
398
|
}
|
|
461
399
|
|
|
462
|
-
|
|
463
|
-
function transformFragment(path, state) {
|
|
400
|
+
function transformFragmentVdom(path, state) {
|
|
464
401
|
const { node } = path;
|
|
465
|
-
const transformedChildren =
|
|
402
|
+
const transformedChildren = transformChildrenVdom(node.children, state);
|
|
466
403
|
|
|
467
404
|
state.needsH = true;
|
|
468
405
|
state.needsFragment = true;
|
|
@@ -473,87 +410,776 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
473
410
|
);
|
|
474
411
|
}
|
|
475
412
|
|
|
413
|
+
// =====================================================
|
|
414
|
+
// Fine-Grained Mode (template + insert + effect)
|
|
415
|
+
// =====================================================
|
|
416
|
+
|
|
417
|
+
let templateCounter = 0;
|
|
418
|
+
|
|
419
|
+
// Check if a JSX child is static (no expressions)
|
|
420
|
+
function isStaticChild(child) {
|
|
421
|
+
if (t.isJSXText(child)) return true;
|
|
422
|
+
if (t.isJSXExpressionContainer(child)) return false;
|
|
423
|
+
if (t.isJSXElement(child)) {
|
|
424
|
+
const el = child.openingElement;
|
|
425
|
+
const tagName = el.name.name;
|
|
426
|
+
if (isComponent(tagName)) return false;
|
|
427
|
+
// Check if attributes are all static
|
|
428
|
+
for (const attr of el.attributes) {
|
|
429
|
+
if (t.isJSXSpreadAttribute(attr)) return false;
|
|
430
|
+
const value = attr.value;
|
|
431
|
+
if (t.isJSXExpressionContainer(value)) return false;
|
|
432
|
+
}
|
|
433
|
+
// Check children recursively
|
|
434
|
+
return child.children.every(isStaticChild);
|
|
435
|
+
}
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check if an attribute value is dynamic (expression, not string literal)
|
|
440
|
+
function isDynamicAttr(attr) {
|
|
441
|
+
if (t.isJSXSpreadAttribute(attr)) return true;
|
|
442
|
+
if (!attr.value) return false; // boolean attr like `disabled`
|
|
443
|
+
return t.isJSXExpressionContainer(attr.value);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Check if an expression is potentially reactive (contains function calls)
|
|
447
|
+
function isPotentiallyReactive(expr) {
|
|
448
|
+
if (t.isCallExpression(expr)) return true;
|
|
449
|
+
if (t.isMemberExpression(expr)) return isPotentiallyReactive(expr.object);
|
|
450
|
+
if (t.isConditionalExpression(expr)) {
|
|
451
|
+
return isPotentiallyReactive(expr.test) || isPotentiallyReactive(expr.consequent) || isPotentiallyReactive(expr.alternate);
|
|
452
|
+
}
|
|
453
|
+
if (t.isBinaryExpression(expr) || t.isLogicalExpression(expr)) {
|
|
454
|
+
return isPotentiallyReactive(expr.left) || isPotentiallyReactive(expr.right);
|
|
455
|
+
}
|
|
456
|
+
if (t.isTemplateLiteral(expr)) {
|
|
457
|
+
return expr.expressions.some(isPotentiallyReactive);
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Extract static HTML from JSX element for template()
|
|
463
|
+
function extractStaticHTML(node) {
|
|
464
|
+
if (t.isJSXText(node)) {
|
|
465
|
+
const text = node.value.replace(/\n\s+/g, ' ').trim();
|
|
466
|
+
return text ? escapeHTML(text) : '';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (t.isJSXExpressionContainer(node)) {
|
|
470
|
+
// Dynamic — leave a placeholder
|
|
471
|
+
return '';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (!t.isJSXElement(node)) return '';
|
|
475
|
+
|
|
476
|
+
const el = node.openingElement;
|
|
477
|
+
const tagName = el.name.name;
|
|
478
|
+
|
|
479
|
+
if (isComponent(tagName)) return '';
|
|
480
|
+
|
|
481
|
+
let html = `<${tagName}`;
|
|
482
|
+
|
|
483
|
+
// Static attributes
|
|
484
|
+
for (const attr of el.attributes) {
|
|
485
|
+
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
486
|
+
const name = attr.name.name;
|
|
487
|
+
if (name.startsWith('on') || name.startsWith('bind:') || name.includes('|')) continue;
|
|
488
|
+
|
|
489
|
+
let domName = name;
|
|
490
|
+
if (name === 'className') domName = 'class';
|
|
491
|
+
if (name === 'htmlFor') domName = 'for';
|
|
492
|
+
|
|
493
|
+
if (!attr.value) {
|
|
494
|
+
html += ` ${domName}`;
|
|
495
|
+
} else if (t.isStringLiteral(attr.value)) {
|
|
496
|
+
html += ` ${domName}="${escapeAttr(attr.value.value)}"`;
|
|
497
|
+
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
498
|
+
// Dynamic attr — skip from template, will be set via effect
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const selfClosing = node.openingElement.selfClosing;
|
|
504
|
+
if (selfClosing) {
|
|
505
|
+
// Void elements
|
|
506
|
+
html += '/>';
|
|
507
|
+
return html;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
html += '>';
|
|
511
|
+
|
|
512
|
+
// Children
|
|
513
|
+
for (const child of node.children) {
|
|
514
|
+
if (t.isJSXText(child)) {
|
|
515
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
516
|
+
if (text) html += escapeHTML(text);
|
|
517
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
518
|
+
// Dynamic child — placeholder will be handled by insert()
|
|
519
|
+
// Skip entirely from template
|
|
520
|
+
} else if (t.isJSXElement(child)) {
|
|
521
|
+
if (isComponent(child.openingElement.name.name)) {
|
|
522
|
+
// Component — skip from template
|
|
523
|
+
} else {
|
|
524
|
+
html += extractStaticHTML(child);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
html += `</${tagName}>`;
|
|
530
|
+
return html;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function escapeHTML(str) {
|
|
534
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function escapeAttr(str) {
|
|
538
|
+
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Analyze JSX tree and generate fine-grained output
|
|
542
|
+
function transformElementFineGrained(path, state) {
|
|
543
|
+
const { node } = path;
|
|
544
|
+
const openingElement = node.openingElement;
|
|
545
|
+
const tagName = openingElement.name.name;
|
|
546
|
+
|
|
547
|
+
if (isComponent(tagName)) {
|
|
548
|
+
return transformComponentFineGrained(path, state);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// For <For> and <Show> control flow components
|
|
552
|
+
if (tagName === 'For') {
|
|
553
|
+
return transformForFineGrained(path, state);
|
|
554
|
+
}
|
|
555
|
+
if (tagName === 'Show') {
|
|
556
|
+
return transformShowFineGrained(path, state);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const attributes = openingElement.attributes;
|
|
560
|
+
const children = node.children;
|
|
561
|
+
|
|
562
|
+
// Check if this entire subtree is purely static
|
|
563
|
+
const allChildrenStatic = children.every(isStaticChild);
|
|
564
|
+
const allAttrsStatic = attributes.every(attr => !isDynamicAttr(attr));
|
|
565
|
+
const noEvents = attributes.every(attr => {
|
|
566
|
+
if (t.isJSXSpreadAttribute(attr)) return false;
|
|
567
|
+
const name = attr.name?.name;
|
|
568
|
+
return !name?.startsWith('on') && !name?.startsWith('bind:');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (allChildrenStatic && allAttrsStatic && noEvents) {
|
|
572
|
+
// Fully static element — extract to template, return clone call
|
|
573
|
+
const html = extractStaticHTML(node);
|
|
574
|
+
if (html) {
|
|
575
|
+
const tmplId = generateTemplateId(state);
|
|
576
|
+
state.templates.push({ id: tmplId, html });
|
|
577
|
+
state.needsTemplate = true;
|
|
578
|
+
return t.callExpression(t.identifier(tmplId), []);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Mixed static/dynamic element — extract template, add effects for dynamic parts
|
|
583
|
+
const html = extractStaticHTML(node);
|
|
584
|
+
if (!html) {
|
|
585
|
+
// Fallback to VDOM mode for degenerate cases
|
|
586
|
+
return transformElementVdom(path, state);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const tmplId = generateTemplateId(state);
|
|
590
|
+
state.templates.push({ id: tmplId, html });
|
|
591
|
+
state.needsTemplate = true;
|
|
592
|
+
|
|
593
|
+
const elId = state.nextVarId();
|
|
594
|
+
|
|
595
|
+
// _el$ = _t$()
|
|
596
|
+
const statements = [
|
|
597
|
+
t.variableDeclaration('const', [
|
|
598
|
+
t.variableDeclarator(t.identifier(elId), t.callExpression(t.identifier(tmplId), []))
|
|
599
|
+
])
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
// Apply dynamic attributes and events
|
|
603
|
+
applyDynamicAttrs(statements, elId, attributes, state);
|
|
604
|
+
|
|
605
|
+
// Handle dynamic children
|
|
606
|
+
applyDynamicChildren(statements, elId, children, node, state);
|
|
607
|
+
|
|
608
|
+
// Return the element — wrap in IIFE
|
|
609
|
+
statements.push(t.returnStatement(t.identifier(elId)));
|
|
610
|
+
|
|
611
|
+
return t.callExpression(
|
|
612
|
+
t.arrowFunctionExpression([], t.blockStatement(statements)),
|
|
613
|
+
[]
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function applyDynamicAttrs(statements, elId, attributes, state) {
|
|
618
|
+
for (const attr of attributes) {
|
|
619
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
620
|
+
// spread(el, props) — use runtime spread
|
|
621
|
+
state.needsSpread = true;
|
|
622
|
+
statements.push(
|
|
623
|
+
t.expressionStatement(
|
|
624
|
+
t.callExpression(t.identifier('_$spread'), [t.identifier(elId), attr.argument])
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const attrName = attr.name.name;
|
|
631
|
+
|
|
632
|
+
// Event handlers
|
|
633
|
+
if (attrName.startsWith('on') && !attrName.includes('|')) {
|
|
634
|
+
const event = attrName.slice(2).toLowerCase();
|
|
635
|
+
const handler = getAttributeValue(attr.value);
|
|
636
|
+
// Direct addEventListener
|
|
637
|
+
statements.push(
|
|
638
|
+
t.expressionStatement(
|
|
639
|
+
t.callExpression(
|
|
640
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
641
|
+
[t.stringLiteral(event), handler]
|
|
642
|
+
)
|
|
643
|
+
)
|
|
644
|
+
);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Event with modifiers
|
|
649
|
+
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
650
|
+
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
651
|
+
const handler = getAttributeValue(attr.value);
|
|
652
|
+
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
653
|
+
const event = eventName.slice(2).toLowerCase();
|
|
654
|
+
|
|
655
|
+
const optionMods = modifiers.filter(m => EVENT_OPTION_MODIFIERS.has(m));
|
|
656
|
+
const addEventArgs = [t.stringLiteral(event), wrappedHandler];
|
|
657
|
+
if (optionMods.length > 0) {
|
|
658
|
+
const optsProps = optionMods.map(m =>
|
|
659
|
+
t.objectProperty(t.identifier(m), t.booleanLiteral(true))
|
|
660
|
+
);
|
|
661
|
+
addEventArgs.push(t.objectExpression(optsProps));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
statements.push(
|
|
665
|
+
t.expressionStatement(
|
|
666
|
+
t.callExpression(
|
|
667
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
668
|
+
addEventArgs
|
|
669
|
+
)
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Binding
|
|
676
|
+
if (isBindingAttribute(attrName)) {
|
|
677
|
+
const bindProp = getBindingProperty(attrName);
|
|
678
|
+
const signalExpr = attr.value.expression;
|
|
679
|
+
state.needsEffect = true;
|
|
680
|
+
|
|
681
|
+
if (bindProp === 'value') {
|
|
682
|
+
// Reactive value binding
|
|
683
|
+
statements.push(
|
|
684
|
+
t.expressionStatement(
|
|
685
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
686
|
+
t.arrowFunctionExpression([], t.assignmentExpression('=',
|
|
687
|
+
t.memberExpression(t.identifier(elId), t.identifier('value')),
|
|
688
|
+
t.callExpression(t.cloneNode(signalExpr), [])
|
|
689
|
+
))
|
|
690
|
+
])
|
|
691
|
+
)
|
|
692
|
+
);
|
|
693
|
+
// Input listener
|
|
694
|
+
statements.push(
|
|
695
|
+
t.expressionStatement(
|
|
696
|
+
t.callExpression(
|
|
697
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
698
|
+
[
|
|
699
|
+
t.stringLiteral('input'),
|
|
700
|
+
t.arrowFunctionExpression(
|
|
701
|
+
[t.identifier('e')],
|
|
702
|
+
t.callExpression(
|
|
703
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
704
|
+
[t.memberExpression(
|
|
705
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
706
|
+
t.identifier('value')
|
|
707
|
+
)]
|
|
708
|
+
)
|
|
709
|
+
)
|
|
710
|
+
]
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Dynamic attribute (expression)
|
|
719
|
+
if (t.isJSXExpressionContainer(attr.value)) {
|
|
720
|
+
const expr = attr.value.expression;
|
|
721
|
+
let domName = attrName;
|
|
722
|
+
if (attrName === 'className') domName = 'class';
|
|
723
|
+
if (attrName === 'htmlFor') domName = 'for';
|
|
724
|
+
|
|
725
|
+
if (isPotentiallyReactive(expr)) {
|
|
726
|
+
// Reactive attribute — wrap in effect
|
|
727
|
+
state.needsEffect = true;
|
|
728
|
+
if (domName === 'class') {
|
|
729
|
+
statements.push(
|
|
730
|
+
t.expressionStatement(
|
|
731
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
732
|
+
t.arrowFunctionExpression([], t.assignmentExpression('=',
|
|
733
|
+
t.memberExpression(t.identifier(elId), t.identifier('className')),
|
|
734
|
+
t.logicalExpression('||', expr, t.stringLiteral(''))
|
|
735
|
+
))
|
|
736
|
+
])
|
|
737
|
+
)
|
|
738
|
+
);
|
|
739
|
+
} else if (domName === 'style') {
|
|
740
|
+
statements.push(
|
|
741
|
+
t.expressionStatement(
|
|
742
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
743
|
+
t.arrowFunctionExpression([], t.blockStatement([
|
|
744
|
+
t.expressionStatement(
|
|
745
|
+
t.callExpression(
|
|
746
|
+
t.memberExpression(
|
|
747
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
748
|
+
t.identifier('call')
|
|
749
|
+
),
|
|
750
|
+
[t.nullLiteral(), t.memberExpression(t.identifier(elId), t.identifier('style')), expr]
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
]))
|
|
754
|
+
])
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
statements.push(
|
|
759
|
+
t.expressionStatement(
|
|
760
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
761
|
+
t.arrowFunctionExpression([], t.callExpression(
|
|
762
|
+
t.memberExpression(t.identifier(elId), t.identifier('setAttribute')),
|
|
763
|
+
[t.stringLiteral(domName), expr]
|
|
764
|
+
))
|
|
765
|
+
])
|
|
766
|
+
)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
// Static expression (no signal calls) — set once
|
|
771
|
+
if (domName === 'class') {
|
|
772
|
+
statements.push(
|
|
773
|
+
t.expressionStatement(
|
|
774
|
+
t.assignmentExpression('=',
|
|
775
|
+
t.memberExpression(t.identifier(elId), t.identifier('className')),
|
|
776
|
+
t.logicalExpression('||', expr, t.stringLiteral(''))
|
|
777
|
+
)
|
|
778
|
+
)
|
|
779
|
+
);
|
|
780
|
+
} else {
|
|
781
|
+
statements.push(
|
|
782
|
+
t.expressionStatement(
|
|
783
|
+
t.callExpression(
|
|
784
|
+
t.memberExpression(t.identifier(elId), t.identifier('setAttribute')),
|
|
785
|
+
[t.stringLiteral(domName), expr]
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Static string/boolean attributes already in template
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function applyDynamicChildren(statements, elId, children, parentNode, state) {
|
|
797
|
+
// Build a child access path. We need to track position relative to template's children.
|
|
798
|
+
// Dynamic children (expressions and components) need insert() calls.
|
|
799
|
+
let childIndex = 0;
|
|
800
|
+
|
|
801
|
+
for (const child of children) {
|
|
802
|
+
if (t.isJSXText(child)) {
|
|
803
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
804
|
+
if (text) childIndex++;
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
809
|
+
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
810
|
+
|
|
811
|
+
const expr = child.expression;
|
|
812
|
+
state.needsInsert = true;
|
|
813
|
+
|
|
814
|
+
// insert(parent, () => expr, marker?)
|
|
815
|
+
// For now use simple insert without marker — appends
|
|
816
|
+
if (isPotentiallyReactive(expr)) {
|
|
817
|
+
statements.push(
|
|
818
|
+
t.expressionStatement(
|
|
819
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
820
|
+
t.identifier(elId),
|
|
821
|
+
t.arrowFunctionExpression([], expr)
|
|
822
|
+
])
|
|
823
|
+
)
|
|
824
|
+
);
|
|
825
|
+
} else {
|
|
826
|
+
statements.push(
|
|
827
|
+
t.expressionStatement(
|
|
828
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
829
|
+
t.identifier(elId),
|
|
830
|
+
expr
|
|
831
|
+
])
|
|
832
|
+
)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (t.isJSXElement(child)) {
|
|
839
|
+
const childTag = child.openingElement.name.name;
|
|
840
|
+
if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
|
|
841
|
+
// Component/control-flow — transform and insert
|
|
842
|
+
const transformed = transformElementFineGrained({ node: child }, state);
|
|
843
|
+
state.needsInsert = true;
|
|
844
|
+
statements.push(
|
|
845
|
+
t.expressionStatement(
|
|
846
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
847
|
+
t.identifier(elId),
|
|
848
|
+
transformed
|
|
849
|
+
])
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
} else {
|
|
853
|
+
// Static child element — already in template
|
|
854
|
+
// But check if it has dynamic children/attrs that need effects
|
|
855
|
+
const hasAnythingDynamic = child.openingElement.attributes.some(isDynamicAttr) ||
|
|
856
|
+
child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && a.name?.name?.startsWith('on')) ||
|
|
857
|
+
!child.children.every(isStaticChild);
|
|
858
|
+
|
|
859
|
+
if (hasAnythingDynamic) {
|
|
860
|
+
// Need to reference this child element and apply effects to it
|
|
861
|
+
const childElId = state.nextVarId();
|
|
862
|
+
statements.push(
|
|
863
|
+
t.variableDeclaration('const', [
|
|
864
|
+
t.variableDeclarator(
|
|
865
|
+
t.identifier(childElId),
|
|
866
|
+
buildChildAccess(elId, childIndex)
|
|
867
|
+
)
|
|
868
|
+
])
|
|
869
|
+
);
|
|
870
|
+
applyDynamicAttrs(statements, childElId, child.openingElement.attributes, state);
|
|
871
|
+
applyDynamicChildren(statements, childElId, child.children, child, state);
|
|
872
|
+
}
|
|
873
|
+
childIndex++;
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (t.isJSXFragment(child)) {
|
|
879
|
+
// Inline fragment children
|
|
880
|
+
for (const fChild of child.children) {
|
|
881
|
+
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
882
|
+
state.needsInsert = true;
|
|
883
|
+
const expr = fChild.expression;
|
|
884
|
+
if (isPotentiallyReactive(expr)) {
|
|
885
|
+
statements.push(
|
|
886
|
+
t.expressionStatement(
|
|
887
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
888
|
+
t.identifier(elId),
|
|
889
|
+
t.arrowFunctionExpression([], expr)
|
|
890
|
+
])
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
} else {
|
|
894
|
+
statements.push(
|
|
895
|
+
t.expressionStatement(
|
|
896
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
897
|
+
t.identifier(elId),
|
|
898
|
+
expr
|
|
899
|
+
])
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function buildChildAccess(elId, index) {
|
|
910
|
+
// Build _el$.children[index] or _el$.firstChild / .firstChild.nextSibling chain
|
|
911
|
+
// Use children[n] for simplicity and readability
|
|
912
|
+
return t.memberExpression(
|
|
913
|
+
t.memberExpression(t.identifier(elId), t.identifier('children')),
|
|
914
|
+
t.numericLiteral(index),
|
|
915
|
+
true // computed
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function transformComponentFineGrained(path, state) {
|
|
920
|
+
// Components in fine-grained mode still use h() for now (backward compat)
|
|
921
|
+
// The component itself decides how to render (vdom or fine-grained)
|
|
922
|
+
return transformComponentVdom(path, state);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function transformForFineGrained(path, state) {
|
|
926
|
+
const { node } = path;
|
|
927
|
+
const attributes = node.openingElement.attributes;
|
|
928
|
+
const children = node.children;
|
|
929
|
+
|
|
930
|
+
// <For each={data}>{(item) => <Row />}</For>
|
|
931
|
+
// → mapArray(data, (item) => ...)
|
|
932
|
+
let eachExpr = null;
|
|
933
|
+
for (const attr of attributes) {
|
|
934
|
+
if (t.isJSXAttribute(attr) && attr.name.name === 'each') {
|
|
935
|
+
eachExpr = getAttributeValue(attr.value);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (!eachExpr) {
|
|
940
|
+
// Fallback
|
|
941
|
+
return transformElementVdom(path, state);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Get the render function from children
|
|
945
|
+
let renderFn = null;
|
|
946
|
+
for (const child of children) {
|
|
947
|
+
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
948
|
+
renderFn = child.expression;
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (!renderFn) {
|
|
954
|
+
return transformElementVdom(path, state);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
state.needsMapArray = true;
|
|
958
|
+
return t.callExpression(t.identifier('_$mapArray'), [eachExpr, renderFn]);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function transformShowFineGrained(path, state) {
|
|
962
|
+
const { node } = path;
|
|
963
|
+
const attributes = node.openingElement.attributes;
|
|
964
|
+
const children = node.children;
|
|
965
|
+
|
|
966
|
+
// <Show when={cond}>{content}</Show>
|
|
967
|
+
// Still uses h(Show, ...) for now — Show is a runtime component
|
|
968
|
+
return transformElementVdom(path, state);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function transformFragmentFineGrained(path, state) {
|
|
972
|
+
const { node } = path;
|
|
973
|
+
const children = node.children;
|
|
974
|
+
|
|
975
|
+
// Fragments with fine-grained: just return children array or single child
|
|
976
|
+
const transformed = [];
|
|
977
|
+
for (const child of children) {
|
|
978
|
+
if (t.isJSXText(child)) {
|
|
979
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
980
|
+
if (text) transformed.push(t.stringLiteral(text));
|
|
981
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
982
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
983
|
+
transformed.push(child.expression);
|
|
984
|
+
}
|
|
985
|
+
} else if (t.isJSXElement(child)) {
|
|
986
|
+
transformed.push(transformElementFineGrained({ node: child }, state));
|
|
987
|
+
} else if (t.isJSXFragment(child)) {
|
|
988
|
+
transformed.push(transformFragmentFineGrained({ node: child }, state));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (transformed.length === 1) return transformed[0];
|
|
993
|
+
return t.arrayExpression(transformed);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function generateTemplateId(state) {
|
|
997
|
+
return `_t$${state.templateCount++}`;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// =====================================================
|
|
1001
|
+
// Plugin entry
|
|
1002
|
+
// =====================================================
|
|
1003
|
+
|
|
476
1004
|
return {
|
|
477
1005
|
name: 'what-jsx-transform',
|
|
478
1006
|
|
|
479
1007
|
visitor: {
|
|
480
1008
|
Program: {
|
|
481
1009
|
enter(path, state) {
|
|
1010
|
+
// Read mode from plugin options
|
|
1011
|
+
const pluginMode = state.opts?.mode || mode;
|
|
1012
|
+
state.mode = pluginMode;
|
|
1013
|
+
|
|
1014
|
+
// VDOM mode state
|
|
482
1015
|
state.needsH = false;
|
|
483
1016
|
state.needsFragment = false;
|
|
484
1017
|
state.needsIsland = false;
|
|
1018
|
+
|
|
1019
|
+
// Fine-grained mode state
|
|
1020
|
+
state.needsTemplate = false;
|
|
1021
|
+
state.needsInsert = false;
|
|
1022
|
+
state.needsEffect = false;
|
|
1023
|
+
state.needsMapArray = false;
|
|
1024
|
+
state.needsSpread = false;
|
|
1025
|
+
state.templates = [];
|
|
1026
|
+
state.templateCount = 0;
|
|
1027
|
+
state._varCounter = 0;
|
|
1028
|
+
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
485
1029
|
},
|
|
486
1030
|
|
|
487
1031
|
exit(path, state) {
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
coreSpecifiers.push(
|
|
501
|
-
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
502
|
-
);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
const importDecl = t.importDeclaration(
|
|
506
|
-
coreSpecifiers,
|
|
507
|
-
t.stringLiteral('what-framework')
|
|
508
|
-
);
|
|
1032
|
+
if (state.mode === 'fine-grained') {
|
|
1033
|
+
// Insert template declarations at top of program
|
|
1034
|
+
for (const tmpl of state.templates.reverse()) {
|
|
1035
|
+
path.unshiftContainer('body',
|
|
1036
|
+
t.variableDeclaration('const', [
|
|
1037
|
+
t.variableDeclarator(
|
|
1038
|
+
t.identifier(tmpl.id),
|
|
1039
|
+
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1040
|
+
)
|
|
1041
|
+
])
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
509
1044
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1045
|
+
// Build imports
|
|
1046
|
+
const fgSpecifiers = [];
|
|
1047
|
+
if (state.needsTemplate) {
|
|
1048
|
+
fgSpecifiers.push(
|
|
1049
|
+
t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (state.needsInsert) {
|
|
1053
|
+
fgSpecifiers.push(
|
|
1054
|
+
t.importSpecifier(t.identifier('_$insert'), t.identifier('insert'))
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
if (state.needsEffect) {
|
|
1058
|
+
fgSpecifiers.push(
|
|
1059
|
+
t.importSpecifier(t.identifier('_$effect'), t.identifier('effect'))
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (state.needsMapArray) {
|
|
1063
|
+
fgSpecifiers.push(
|
|
1064
|
+
t.importSpecifier(t.identifier('_$mapArray'), t.identifier('mapArray'))
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
if (state.needsSpread) {
|
|
1068
|
+
fgSpecifiers.push(
|
|
1069
|
+
t.importSpecifier(t.identifier('_$spread'), t.identifier('spread'))
|
|
1070
|
+
);
|
|
516
1071
|
}
|
|
517
|
-
}
|
|
518
1072
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1073
|
+
// Also include h/Fragment/Island if vdom mode used for components
|
|
1074
|
+
const coreSpecifiers = [];
|
|
1075
|
+
if (state.needsH) {
|
|
1076
|
+
coreSpecifiers.push(
|
|
1077
|
+
t.importSpecifier(t.identifier('h'), t.identifier('h'))
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (state.needsFragment) {
|
|
1081
|
+
coreSpecifiers.push(
|
|
1082
|
+
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (state.needsIsland) {
|
|
1086
|
+
coreSpecifiers.push(
|
|
1087
|
+
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
1088
|
+
);
|
|
526
1089
|
}
|
|
527
|
-
}
|
|
528
1090
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.
|
|
534
|
-
|
|
535
|
-
|
|
1091
|
+
if (fgSpecifiers.length > 0) {
|
|
1092
|
+
// Check for existing render import
|
|
1093
|
+
let existingRenderImport = null;
|
|
1094
|
+
for (const node of path.node.body) {
|
|
1095
|
+
if (t.isImportDeclaration(node) && (
|
|
1096
|
+
node.source.value === 'what-framework/render' ||
|
|
1097
|
+
node.source.value === 'what-core/render'
|
|
1098
|
+
)) {
|
|
1099
|
+
existingRenderImport = node;
|
|
1100
|
+
break;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
536
1103
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1104
|
+
if (!existingRenderImport) {
|
|
1105
|
+
path.unshiftContainer('body',
|
|
1106
|
+
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1107
|
+
);
|
|
540
1108
|
}
|
|
541
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
if (coreSpecifiers.length > 0) {
|
|
1112
|
+
addCoreImports(path, t, coreSpecifiers);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
542
1115
|
} else {
|
|
543
|
-
|
|
1116
|
+
// VDOM mode
|
|
1117
|
+
if (!state.needsH) return;
|
|
1118
|
+
|
|
1119
|
+
const coreSpecifiers = [
|
|
1120
|
+
t.importSpecifier(t.identifier('h'), t.identifier('h')),
|
|
1121
|
+
];
|
|
1122
|
+
if (state.needsFragment) {
|
|
1123
|
+
coreSpecifiers.push(
|
|
1124
|
+
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (state.needsIsland) {
|
|
1128
|
+
coreSpecifiers.push(
|
|
1129
|
+
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
addCoreImports(path, t, coreSpecifiers);
|
|
544
1134
|
}
|
|
545
1135
|
}
|
|
546
1136
|
},
|
|
547
1137
|
|
|
548
1138
|
JSXElement(path, state) {
|
|
549
|
-
const transformed =
|
|
1139
|
+
const transformed = state.mode === 'fine-grained'
|
|
1140
|
+
? transformElementFineGrained(path, state)
|
|
1141
|
+
: transformElementVdom(path, state);
|
|
550
1142
|
path.replaceWith(transformed);
|
|
551
1143
|
},
|
|
552
1144
|
|
|
553
1145
|
JSXFragment(path, state) {
|
|
554
|
-
const transformed =
|
|
1146
|
+
const transformed = state.mode === 'fine-grained'
|
|
1147
|
+
? transformFragmentFineGrained(path, state)
|
|
1148
|
+
: transformFragmentVdom(path, state);
|
|
555
1149
|
path.replaceWith(transformed);
|
|
556
1150
|
}
|
|
557
1151
|
}
|
|
558
1152
|
};
|
|
559
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
function addCoreImports(path, t, coreSpecifiers) {
|
|
1156
|
+
let existingImport = null;
|
|
1157
|
+
for (const node of path.node.body) {
|
|
1158
|
+
if (t.isImportDeclaration(node) && (
|
|
1159
|
+
node.source.value === 'what-core' || node.source.value === 'what-framework'
|
|
1160
|
+
)) {
|
|
1161
|
+
existingImport = node;
|
|
1162
|
+
break;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (existingImport) {
|
|
1167
|
+
const existingNames = new Set(
|
|
1168
|
+
existingImport.specifiers
|
|
1169
|
+
.filter(s => t.isImportSpecifier(s))
|
|
1170
|
+
.map(s => s.imported.name)
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
for (const spec of coreSpecifiers) {
|
|
1174
|
+
if (!existingNames.has(spec.imported.name)) {
|
|
1175
|
+
existingImport.specifiers.push(spec);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
} else {
|
|
1179
|
+
const importDecl = t.importDeclaration(
|
|
1180
|
+
coreSpecifiers,
|
|
1181
|
+
t.stringLiteral('what-framework')
|
|
1182
|
+
);
|
|
1183
|
+
path.unshiftContainer('body', importDecl);
|
|
1184
|
+
}
|
|
1185
|
+
}
|