what-compiler 0.5.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/babel-plugin.js +1307 -0
- package/dist/babel-plugin.js.map +7 -0
- package/dist/babel-plugin.min.js +2 -0
- package/dist/babel-plugin.min.js.map +7 -0
- package/dist/file-router.js +195 -0
- package/dist/file-router.js.map +7 -0
- package/dist/file-router.min.js +3 -0
- package/dist/file-router.min.js.map +7 -0
- package/dist/index.js +2065 -0
- package/dist/index.js.map +7 -0
- package/dist/index.min.js +397 -0
- package/dist/index.min.js.map +7 -0
- package/dist/runtime.js +9 -0
- package/dist/runtime.js.map +7 -0
- package/dist/runtime.min.js +2 -0
- package/dist/runtime.min.js.map +7 -0
- package/dist/vite-plugin.js +2054 -0
- package/dist/vite-plugin.js.map +7 -0
- package/dist/vite-plugin.min.js +397 -0
- package/dist/vite-plugin.min.js.map +7 -0
- package/package.json +26 -10
- package/src/babel-plugin.js +943 -559
- package/src/error-overlay.js +190 -119
- package/src/file-router.js +2 -1
- package/src/vite-plugin.js +86 -3
package/src/babel-plugin.js
CHANGED
|
@@ -1,47 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What Framework Babel Plugin
|
|
2
|
+
* What Framework Babel Plugin — Fine-Grained Only
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Static HTML extracted to templates, dynamic expressions wrapped in effects.
|
|
4
|
+
* JSX → template() + insert() + effect() calls
|
|
5
|
+
* Static HTML extracted to module-level templates, dynamic expressions wrapped in effects.
|
|
6
|
+
* Components run ONCE. All reactivity is signal-driven.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
* const
|
|
8
|
+
* Output:
|
|
9
|
+
* const _tmpl$1 = template('<div class="container"><h1>Title</h1><p></p></div>');
|
|
11
10
|
* function App() {
|
|
12
|
-
* const _el$ =
|
|
13
|
-
* insert(_el$.
|
|
11
|
+
* const _el$ = _tmpl$1();
|
|
12
|
+
* insert(_el$.childNodes[1], () => desc());
|
|
14
13
|
* return _el$;
|
|
15
14
|
* }
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Template calls are hoisted to module scope — each unique HTML string gets one
|
|
17
|
+
* top-level const. Component functions just clone: `const _el$ = _tmpl$1()`.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
const EVENT_MODIFIERS = new Set(['preventDefault', 'stopPropagation', 'once', 'capture', 'passive', 'self']);
|
|
22
21
|
const EVENT_OPTION_MODIFIERS = new Set(['once', 'capture', 'passive']);
|
|
23
22
|
const VOID_HTML_ELEMENTS = new Set([
|
|
24
|
-
'area',
|
|
25
|
-
'
|
|
26
|
-
'br',
|
|
27
|
-
'col',
|
|
28
|
-
'embed',
|
|
29
|
-
'hr',
|
|
30
|
-
'img',
|
|
31
|
-
'input',
|
|
32
|
-
'link',
|
|
33
|
-
'meta',
|
|
34
|
-
'param',
|
|
35
|
-
'source',
|
|
36
|
-
'track',
|
|
37
|
-
'wbr'
|
|
23
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
24
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
38
25
|
]);
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
// Events that use document-level delegation for performance.
|
|
28
|
+
// The compiler emits `el.__click = handler` instead of addEventListener.
|
|
29
|
+
// A one-time document listener walks event.target upward to find the handler.
|
|
30
|
+
const DELEGATED_EVENTS = new Set([
|
|
31
|
+
'click', 'input', 'change', 'keydown', 'keyup', 'submit',
|
|
32
|
+
'focusin', 'focusout', 'mousedown', 'mouseup',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
// Known non-reactive call expressions — these should NOT be wrapped in effects
|
|
36
|
+
// unless their arguments contain signal reads.
|
|
37
|
+
const SAFE_GLOBAL_CALLS = new Set([
|
|
38
|
+
'Math', 'Number', 'String', 'Boolean', 'parseInt', 'parseFloat',
|
|
39
|
+
'isNaN', 'isFinite', 'encodeURIComponent', 'decodeURIComponent',
|
|
40
|
+
'encodeURI', 'decodeURI', 'JSON', 'Date', 'Array', 'Object',
|
|
41
|
+
'console', 'RegExp',
|
|
42
|
+
]);
|
|
42
43
|
|
|
44
|
+
// Known signal-creating functions
|
|
45
|
+
const SIGNAL_CREATORS = new Set([
|
|
46
|
+
'useSignal', 'signal', 'computed', 'useComputed', 'useState', 'useReducer',
|
|
47
|
+
'createResource', 'useSWR', 'useQuery', 'useInfiniteQuery',
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
export default function whatBabelPlugin({ types: t }) {
|
|
43
51
|
// =====================================================
|
|
44
|
-
// Shared utilities
|
|
52
|
+
// Shared utilities
|
|
45
53
|
// =====================================================
|
|
46
54
|
|
|
47
55
|
function parseEventModifiers(name) {
|
|
@@ -80,6 +88,14 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
80
88
|
return attrName;
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
// Safely extract attribute name, handling JSXNamespacedName (e.g., client:idle, bind:value)
|
|
92
|
+
function getAttrName(attr) {
|
|
93
|
+
if (t.isJSXNamespacedName(attr.name)) {
|
|
94
|
+
return `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
95
|
+
}
|
|
96
|
+
return typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
|
|
97
|
+
}
|
|
98
|
+
|
|
83
99
|
function createEventHandler(handler, modifiers) {
|
|
84
100
|
if (modifiers.length === 0) return handler;
|
|
85
101
|
|
|
@@ -150,298 +166,227 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
// =====================================================
|
|
153
|
-
//
|
|
169
|
+
// Reactivity Detection — Signal-Aware
|
|
154
170
|
// =====================================================
|
|
155
171
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (t.isJSXText(child)) {
|
|
160
|
-
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
161
|
-
if (text) result.push(t.stringLiteral(text));
|
|
162
|
-
} else if (t.isJSXExpressionContainer(child)) {
|
|
163
|
-
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
164
|
-
result.push(child.expression);
|
|
165
|
-
}
|
|
166
|
-
} else if (t.isJSXElement(child)) {
|
|
167
|
-
result.push(transformElementVdom({ node: child }, state));
|
|
168
|
-
} else if (t.isJSXFragment(child)) {
|
|
169
|
-
result.push(transformFragmentVdom({ node: child }, state));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return result;
|
|
172
|
+
// Check if an identifier is known to be a signal (from useSignal/signal/computed/useState)
|
|
173
|
+
function isSignalIdentifier(name, signalNames) {
|
|
174
|
+
return signalNames.has(name);
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
hasSpread = true;
|
|
193
|
-
spreadExpr = attr.argument;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const attrName = typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
|
|
198
|
-
|
|
199
|
-
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
200
|
-
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
201
|
-
const handler = getAttributeValue(attr.value);
|
|
202
|
-
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
203
|
-
|
|
204
|
-
const optionMods = modifiers.filter(m => EVENT_OPTION_MODIFIERS.has(m));
|
|
205
|
-
if (optionMods.length > 0) {
|
|
206
|
-
const tempId = path.scope
|
|
207
|
-
? path.scope.generateUidIdentifier('handler')
|
|
208
|
-
: t.identifier('_h' + Math.random().toString(36).slice(2, 6));
|
|
209
|
-
|
|
210
|
-
const optsProps = optionMods.map(m =>
|
|
211
|
-
t.objectProperty(t.identifier(m), t.booleanLiteral(true))
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const iifeHandler = t.callExpression(
|
|
215
|
-
t.arrowFunctionExpression(
|
|
216
|
-
[],
|
|
217
|
-
t.blockStatement([
|
|
218
|
-
t.variableDeclaration('const', [
|
|
219
|
-
t.variableDeclarator(tempId, wrappedHandler)
|
|
220
|
-
]),
|
|
221
|
-
t.expressionStatement(
|
|
222
|
-
t.assignmentExpression(
|
|
223
|
-
'=',
|
|
224
|
-
t.memberExpression(t.cloneNode(tempId), t.identifier('_eventOpts')),
|
|
225
|
-
t.objectExpression(optsProps)
|
|
226
|
-
)
|
|
227
|
-
),
|
|
228
|
-
t.returnStatement(t.cloneNode(tempId))
|
|
229
|
-
])
|
|
230
|
-
),
|
|
231
|
-
[]
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
props.push(t.objectProperty(t.identifier(eventName), iifeHandler));
|
|
235
|
-
} else {
|
|
236
|
-
props.push(t.objectProperty(t.identifier(eventName), wrappedHandler));
|
|
237
|
-
}
|
|
238
|
-
continue;
|
|
177
|
+
// Collect signal identifiers using Babel's scope analysis.
|
|
178
|
+
// Walks the scope chain from the given path upward, collecting signals
|
|
179
|
+
// defined in each lexical scope (function/block).
|
|
180
|
+
function collectSignalNamesFromScope(path) {
|
|
181
|
+
const signalNames = new Set();
|
|
182
|
+
|
|
183
|
+
// Helper: extract signal names from a VariableDeclarator node
|
|
184
|
+
function extractFromDeclarator(decl) {
|
|
185
|
+
const init = decl.init;
|
|
186
|
+
if (!init || !t.isCallExpression(init)) return;
|
|
187
|
+
|
|
188
|
+
const callee = init.callee;
|
|
189
|
+
let calleeName = '';
|
|
190
|
+
if (t.isIdentifier(callee)) {
|
|
191
|
+
calleeName = callee.name;
|
|
192
|
+
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
193
|
+
calleeName = callee.property.name;
|
|
239
194
|
}
|
|
240
195
|
|
|
241
|
-
if (
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
256
|
-
[t.memberExpression(
|
|
257
|
-
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
258
|
-
t.identifier('value')
|
|
259
|
-
)]
|
|
260
|
-
)
|
|
261
|
-
)
|
|
262
|
-
)
|
|
263
|
-
);
|
|
264
|
-
} else if (bindProp === 'checked') {
|
|
265
|
-
props.push(
|
|
266
|
-
t.objectProperty(t.identifier('checked'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
267
|
-
);
|
|
268
|
-
props.push(
|
|
269
|
-
t.objectProperty(
|
|
270
|
-
t.identifier('onChange'),
|
|
271
|
-
t.arrowFunctionExpression(
|
|
272
|
-
[t.identifier('e')],
|
|
273
|
-
t.callExpression(
|
|
274
|
-
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
275
|
-
[t.memberExpression(
|
|
276
|
-
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
277
|
-
t.identifier('checked')
|
|
278
|
-
)]
|
|
279
|
-
)
|
|
280
|
-
)
|
|
281
|
-
)
|
|
282
|
-
);
|
|
196
|
+
if (SIGNAL_CREATORS.has(calleeName)) {
|
|
197
|
+
const id = decl.id;
|
|
198
|
+
if (t.isIdentifier(id)) {
|
|
199
|
+
signalNames.add(id.name);
|
|
200
|
+
} else if (t.isArrayPattern(id)) {
|
|
201
|
+
for (const el of id.elements) {
|
|
202
|
+
if (t.isIdentifier(el)) signalNames.add(el.name);
|
|
203
|
+
}
|
|
204
|
+
} else if (t.isObjectPattern(id)) {
|
|
205
|
+
for (const prop of id.properties) {
|
|
206
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
207
|
+
signalNames.add(prop.value.name);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
283
210
|
}
|
|
284
|
-
continue;
|
|
285
211
|
}
|
|
286
|
-
|
|
287
|
-
const value = getAttributeValue(attr.value);
|
|
288
|
-
let domAttrName = attrName;
|
|
289
|
-
if (attrName === 'className') domAttrName = 'class';
|
|
290
|
-
if (attrName === 'htmlFor') domAttrName = 'for';
|
|
291
|
-
|
|
292
|
-
props.push(
|
|
293
|
-
t.objectProperty(
|
|
294
|
-
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(domAttrName)
|
|
295
|
-
? t.identifier(domAttrName)
|
|
296
|
-
: t.stringLiteral(domAttrName),
|
|
297
|
-
value
|
|
298
|
-
)
|
|
299
|
-
);
|
|
300
212
|
}
|
|
301
213
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
214
|
+
// Walk up the scope chain using Babel's scope API
|
|
215
|
+
let scope = path.scope;
|
|
216
|
+
while (scope) {
|
|
217
|
+
// Check all bindings in this scope
|
|
218
|
+
for (const [name, binding] of Object.entries(scope.bindings)) {
|
|
219
|
+
if (binding.path.isVariableDeclarator()) {
|
|
220
|
+
extractFromDeclarator(binding.path.node);
|
|
221
|
+
}
|
|
222
|
+
// Also check function params (destructured props)
|
|
223
|
+
if (binding.path.isIdentifier() || binding.kind === 'param') {
|
|
224
|
+
const fnPath = binding.scope.path;
|
|
225
|
+
if (fnPath && fnPath.node && fnPath.node.params) {
|
|
226
|
+
for (const param of fnPath.node.params) {
|
|
227
|
+
if (t.isObjectPattern(param)) {
|
|
228
|
+
for (const prop of param.properties) {
|
|
229
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
230
|
+
signalNames.add(prop.value.name);
|
|
231
|
+
} else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
|
|
232
|
+
signalNames.add(prop.argument.name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
313
239
|
}
|
|
314
|
-
|
|
315
|
-
propsExpr = t.objectExpression(props);
|
|
316
|
-
} else {
|
|
317
|
-
propsExpr = t.nullLiteral();
|
|
240
|
+
scope = scope.parent;
|
|
318
241
|
}
|
|
319
242
|
|
|
320
|
-
|
|
321
|
-
state.needsH = true;
|
|
322
|
-
return t.callExpression(t.identifier('h'), args);
|
|
243
|
+
return signalNames;
|
|
323
244
|
}
|
|
324
245
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const attributes = openingElement.attributes;
|
|
330
|
-
const children = node.children;
|
|
246
|
+
// Legacy wrapper for backward compat (used in collectSignalNames calls)
|
|
247
|
+
function collectSignalNames(path) {
|
|
248
|
+
return collectSignalNamesFromScope(path);
|
|
249
|
+
}
|
|
331
250
|
|
|
332
|
-
|
|
333
|
-
|
|
251
|
+
// Check if a call expression is a safe (non-reactive) global call
|
|
252
|
+
function isSafeGlobalCall(expr) {
|
|
253
|
+
if (!t.isCallExpression(expr)) return false;
|
|
254
|
+
const callee = expr.callee;
|
|
334
255
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (name && name.startsWith('client:')) {
|
|
339
|
-
const mode = name.slice(7);
|
|
340
|
-
if (attr.value) {
|
|
341
|
-
clientDirective = { type: mode, value: attr.value.value };
|
|
342
|
-
} else {
|
|
343
|
-
clientDirective = { type: mode };
|
|
344
|
-
}
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
filteredAttrs.push(attr);
|
|
256
|
+
// Math.max(), Number.parseInt(), etc.
|
|
257
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.object)) {
|
|
258
|
+
return SAFE_GLOBAL_CALLS.has(callee.object.name);
|
|
349
259
|
}
|
|
350
260
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
261
|
+
// parseInt(), isNaN(), etc.
|
|
262
|
+
if (t.isIdentifier(callee)) {
|
|
263
|
+
return SAFE_GLOBAL_CALLS.has(callee.name);
|
|
264
|
+
}
|
|
354
265
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
358
|
-
];
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
359
268
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
269
|
+
// Check if an expression's reactivity is uncertain — e.g., a non-signal function call
|
|
270
|
+
// whose arguments happen to contain signal reads. The function itself may not produce
|
|
271
|
+
// a reactive result, so the compiler wraps it conservatively.
|
|
272
|
+
function isUncertainReactive(expr, signalNames, importedIds) {
|
|
273
|
+
if (!signalNames) return false;
|
|
274
|
+
if (t.isCallExpression(expr)) {
|
|
275
|
+
// Callee is a known signal — definitely reactive, not uncertain
|
|
276
|
+
if (t.isIdentifier(expr.callee) && isSignalIdentifier(expr.callee.name, signalNames)) {
|
|
277
|
+
return false;
|
|
364
278
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
279
|
+
// Imported identifier called as function — definitely reactive (not uncertain)
|
|
280
|
+
if (importedIds && t.isIdentifier(expr.callee) && importedIds.has(expr.callee.name) &&
|
|
281
|
+
!SAFE_GLOBAL_CALLS.has(expr.callee.name)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
// Callee is a member of a known signal — definitely reactive
|
|
285
|
+
if (t.isMemberExpression(expr.callee) && t.isIdentifier(expr.callee.object) &&
|
|
286
|
+
isSignalIdentifier(expr.callee.object.name, signalNames)) {
|
|
287
|
+
return false;
|
|
371
288
|
}
|
|
289
|
+
// Safe global call (Math.max, etc.) with reactive args — still deterministic, not uncertain
|
|
290
|
+
if (isSafeGlobalCall(expr)) return false;
|
|
291
|
+
// Unknown function call — if args are reactive, the wrapping is uncertain
|
|
292
|
+
if (expr.arguments.some(arg => isPotentiallyReactive(arg, signalNames, importedIds))) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
372
298
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
299
|
+
// Check if an expression is potentially reactive (reads a signal)
|
|
300
|
+
// importedIds: Set of identifiers imported from other modules — any imported
|
|
301
|
+
// function call is conservatively treated as potentially reactive since the
|
|
302
|
+
// imported binding could be a signal from another file.
|
|
303
|
+
function isPotentiallyReactive(expr, signalNames, importedIds) {
|
|
304
|
+
if (!signalNames) signalNames = new Set();
|
|
305
|
+
|
|
306
|
+
if (t.isCallExpression(expr)) {
|
|
307
|
+
// If callee is a known signal identifier being called (signal read), it's reactive
|
|
308
|
+
if (t.isIdentifier(expr.callee) && isSignalIdentifier(expr.callee.name, signalNames)) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
// Imported identifier called as a function — conservatively reactive.
|
|
312
|
+
// Handles: import { count } from './store'; ... {count()} in JSX
|
|
313
|
+
if (importedIds && t.isIdentifier(expr.callee) && importedIds.has(expr.callee.name)) {
|
|
314
|
+
// Exclude known safe globals that happen to also be imported
|
|
315
|
+
if (!SAFE_GLOBAL_CALLS.has(expr.callee.name)) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// member.call() — e.g., data(), isLoading()
|
|
320
|
+
if (t.isMemberExpression(expr.callee)) {
|
|
321
|
+
// Check if the object is a signal
|
|
322
|
+
if (t.isIdentifier(expr.callee.object) && isSignalIdentifier(expr.callee.object.name, signalNames)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Safe global calls like Math.max — only reactive if their args are
|
|
327
|
+
if (isSafeGlobalCall(expr)) {
|
|
328
|
+
return expr.arguments.some(arg => isPotentiallyReactive(arg, signalNames, importedIds));
|
|
329
|
+
}
|
|
330
|
+
// Unknown call — check if callee or args contain signal reads
|
|
331
|
+
if (t.isIdentifier(expr.callee)) {
|
|
332
|
+
// Could be a function that reads signals internally
|
|
333
|
+
// Be conservative: if it's not a known safe call and not a signal, still check args
|
|
334
|
+
return expr.arguments.some(arg => isPotentiallyReactive(arg, signalNames, importedIds));
|
|
335
|
+
}
|
|
336
|
+
// For any other call expression, check recursively
|
|
337
|
+
return isPotentiallyReactive(expr.callee, signalNames, importedIds) ||
|
|
338
|
+
expr.arguments.some(arg => isPotentiallyReactive(arg, signalNames, importedIds));
|
|
377
339
|
}
|
|
378
340
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
341
|
+
if (t.isIdentifier(expr)) {
|
|
342
|
+
return isSignalIdentifier(expr.name, signalNames);
|
|
343
|
+
}
|
|
382
344
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
spreadExpr = attr.argument;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
345
|
+
if (t.isMemberExpression(expr)) {
|
|
346
|
+
return isPotentiallyReactive(expr.object, signalNames, importedIds);
|
|
347
|
+
}
|
|
389
348
|
|
|
390
|
-
|
|
391
|
-
|
|
349
|
+
if (t.isConditionalExpression(expr)) {
|
|
350
|
+
return isPotentiallyReactive(expr.test, signalNames, importedIds) ||
|
|
351
|
+
isPotentiallyReactive(expr.consequent, signalNames, importedIds) ||
|
|
352
|
+
isPotentiallyReactive(expr.alternate, signalNames, importedIds);
|
|
353
|
+
}
|
|
392
354
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
? t.identifier(attrName)
|
|
397
|
-
: t.stringLiteral(attrName),
|
|
398
|
-
value
|
|
399
|
-
)
|
|
400
|
-
);
|
|
355
|
+
if (t.isBinaryExpression(expr) || t.isLogicalExpression(expr)) {
|
|
356
|
+
return isPotentiallyReactive(expr.left, signalNames, importedIds) ||
|
|
357
|
+
isPotentiallyReactive(expr.right, signalNames, importedIds);
|
|
401
358
|
}
|
|
402
359
|
|
|
403
|
-
|
|
360
|
+
if (t.isUnaryExpression(expr)) {
|
|
361
|
+
return isPotentiallyReactive(expr.argument, signalNames, importedIds);
|
|
362
|
+
}
|
|
404
363
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (props.length > 0) {
|
|
408
|
-
propsExpr = t.callExpression(
|
|
409
|
-
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
410
|
-
[t.objectExpression([]), spreadExpr, t.objectExpression(props)]
|
|
411
|
-
);
|
|
412
|
-
} else {
|
|
413
|
-
propsExpr = spreadExpr;
|
|
414
|
-
}
|
|
415
|
-
} else if (props.length > 0) {
|
|
416
|
-
propsExpr = t.objectExpression(props);
|
|
417
|
-
} else {
|
|
418
|
-
propsExpr = t.nullLiteral();
|
|
364
|
+
if (t.isTemplateLiteral(expr)) {
|
|
365
|
+
return expr.expressions.some(e => isPotentiallyReactive(e, signalNames, importedIds));
|
|
419
366
|
}
|
|
420
367
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
368
|
+
if (t.isObjectExpression(expr)) {
|
|
369
|
+
return expr.properties.some(prop =>
|
|
370
|
+
t.isObjectProperty(prop) && isPotentiallyReactive(prop.value, signalNames, importedIds)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
425
373
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
374
|
+
if (t.isArrayExpression(expr)) {
|
|
375
|
+
return expr.elements.some(el => el && isPotentiallyReactive(el, signalNames, importedIds));
|
|
376
|
+
}
|
|
429
377
|
|
|
430
|
-
|
|
431
|
-
|
|
378
|
+
if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
|
|
379
|
+
// Function expressions are not reactive themselves — they're callbacks
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
432
382
|
|
|
433
|
-
return
|
|
434
|
-
t.identifier('h'),
|
|
435
|
-
[t.identifier('Fragment'), t.nullLiteral(), ...transformedChildren]
|
|
436
|
-
);
|
|
383
|
+
return false;
|
|
437
384
|
}
|
|
438
385
|
|
|
439
386
|
// =====================================================
|
|
440
387
|
// Fine-Grained Mode (template + insert + effect)
|
|
441
388
|
// =====================================================
|
|
442
389
|
|
|
443
|
-
let templateCounter = 0;
|
|
444
|
-
|
|
445
390
|
// Check if a JSX child is static (no expressions)
|
|
446
391
|
function isStaticChild(child) {
|
|
447
392
|
if (t.isJSXText(child)) return true;
|
|
@@ -450,60 +395,33 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
450
395
|
const el = child.openingElement;
|
|
451
396
|
const tagName = el.name.name;
|
|
452
397
|
if (isComponent(tagName)) return false;
|
|
453
|
-
// Check if attributes are all static
|
|
454
398
|
for (const attr of el.attributes) {
|
|
455
399
|
if (t.isJSXSpreadAttribute(attr)) return false;
|
|
456
400
|
const value = attr.value;
|
|
457
401
|
if (t.isJSXExpressionContainer(value)) return false;
|
|
458
402
|
}
|
|
459
|
-
// Check children recursively
|
|
460
403
|
return child.children.every(isStaticChild);
|
|
461
404
|
}
|
|
462
405
|
return false;
|
|
463
406
|
}
|
|
464
407
|
|
|
465
|
-
// Check if an attribute value is dynamic
|
|
408
|
+
// Check if an attribute value is dynamic
|
|
466
409
|
function isDynamicAttr(attr) {
|
|
467
410
|
if (t.isJSXSpreadAttribute(attr)) return true;
|
|
468
|
-
if (!attr.value) return false;
|
|
411
|
+
if (!attr.value) return false;
|
|
469
412
|
return t.isJSXExpressionContainer(attr.value);
|
|
470
413
|
}
|
|
471
414
|
|
|
472
|
-
//
|
|
473
|
-
function
|
|
474
|
-
if (t.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return isPotentiallyReactive(expr.test) || isPotentiallyReactive(expr.consequent) || isPotentiallyReactive(expr.alternate);
|
|
415
|
+
// Extract static HTML from JSX element for template()
|
|
416
|
+
function extractStaticHTML(node) {
|
|
417
|
+
if (t.isJSXText(node)) {
|
|
418
|
+
const text = node.value.replace(/\n\s+/g, ' ').trim();
|
|
419
|
+
return text ? escapeHTML(text) : '';
|
|
478
420
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return expr.expressions.some(isPotentiallyReactive);
|
|
484
|
-
}
|
|
485
|
-
if (t.isObjectExpression(expr)) {
|
|
486
|
-
return expr.properties.some(prop =>
|
|
487
|
-
t.isObjectProperty(prop) && isPotentiallyReactive(prop.value)
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
if (t.isArrayExpression(expr)) {
|
|
491
|
-
return expr.elements.some(el => el && isPotentiallyReactive(el));
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Extract static HTML from JSX element for template()
|
|
497
|
-
function extractStaticHTML(node) {
|
|
498
|
-
if (t.isJSXText(node)) {
|
|
499
|
-
const text = node.value.replace(/\n\s+/g, ' ').trim();
|
|
500
|
-
return text ? escapeHTML(text) : '';
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
if (t.isJSXExpressionContainer(node)) {
|
|
504
|
-
// Dynamic child marker so insert() can preserve source ordering
|
|
505
|
-
if (t.isJSXEmptyExpression(node.expression)) return '';
|
|
506
|
-
return '<!--$-->';
|
|
421
|
+
|
|
422
|
+
if (t.isJSXExpressionContainer(node)) {
|
|
423
|
+
if (t.isJSXEmptyExpression(node.expression)) return '';
|
|
424
|
+
return '<!--$-->';
|
|
507
425
|
}
|
|
508
426
|
|
|
509
427
|
if (!t.isJSXElement(node)) return '';
|
|
@@ -515,10 +433,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
515
433
|
|
|
516
434
|
let html = `<${tagName}`;
|
|
517
435
|
|
|
518
|
-
// Static attributes
|
|
519
436
|
for (const attr of el.attributes) {
|
|
520
437
|
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
521
|
-
const name = attr
|
|
438
|
+
const name = getAttrName(attr);
|
|
522
439
|
if (name.startsWith('on') || name.startsWith('bind:') || name.includes('|')) continue;
|
|
523
440
|
|
|
524
441
|
let domName = name;
|
|
@@ -530,8 +447,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
530
447
|
} else if (t.isStringLiteral(attr.value)) {
|
|
531
448
|
html += ` ${domName}="${escapeAttr(attr.value.value)}"`;
|
|
532
449
|
} else if (t.isJSXExpressionContainer(attr.value)) {
|
|
533
|
-
// Dynamic attr —
|
|
534
|
-
continue;
|
|
450
|
+
continue; // Dynamic attr — set via effect
|
|
535
451
|
}
|
|
536
452
|
}
|
|
537
453
|
|
|
@@ -548,7 +464,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
548
464
|
|
|
549
465
|
html += '>';
|
|
550
466
|
|
|
551
|
-
// Children
|
|
552
467
|
for (const child of node.children) {
|
|
553
468
|
if (t.isJSXText(child)) {
|
|
554
469
|
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
@@ -575,7 +490,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
575
490
|
}
|
|
576
491
|
|
|
577
492
|
function escapeAttr(str) {
|
|
578
|
-
return str.replace(/&/g, '&').replace(/"/g, '"');
|
|
493
|
+
return str.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
579
494
|
}
|
|
580
495
|
|
|
581
496
|
// Analyze JSX tree and generate fine-grained output
|
|
@@ -588,7 +503,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
588
503
|
return transformComponentFineGrained(path, state);
|
|
589
504
|
}
|
|
590
505
|
|
|
591
|
-
//
|
|
506
|
+
// Control flow components (lowercase but special)
|
|
592
507
|
if (tagName === 'For') {
|
|
593
508
|
return transformForFineGrained(path, state);
|
|
594
509
|
}
|
|
@@ -604,7 +519,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
604
519
|
const allAttrsStatic = attributes.every(attr => !isDynamicAttr(attr));
|
|
605
520
|
const noEvents = attributes.every(attr => {
|
|
606
521
|
if (t.isJSXSpreadAttribute(attr)) return false;
|
|
607
|
-
const name = attr
|
|
522
|
+
const name = getAttrName(attr);
|
|
608
523
|
return !name?.startsWith('on') && !name?.startsWith('bind:');
|
|
609
524
|
});
|
|
610
525
|
|
|
@@ -612,8 +527,7 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
612
527
|
// Fully static element — extract to template, return clone call
|
|
613
528
|
const html = extractStaticHTML(node);
|
|
614
529
|
if (html) {
|
|
615
|
-
const tmplId =
|
|
616
|
-
state.templates.push({ id: tmplId, html });
|
|
530
|
+
const tmplId = getOrCreateTemplate(state, html);
|
|
617
531
|
state.needsTemplate = true;
|
|
618
532
|
return t.callExpression(t.identifier(tmplId), []);
|
|
619
533
|
}
|
|
@@ -622,17 +536,26 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
622
536
|
// Mixed static/dynamic element — extract template, add effects for dynamic parts
|
|
623
537
|
const html = extractStaticHTML(node);
|
|
624
538
|
if (!html) {
|
|
625
|
-
//
|
|
626
|
-
|
|
539
|
+
// Template extraction failed — emit a detailed compile warning and use h() as fallback
|
|
540
|
+
const loc = node.loc;
|
|
541
|
+
const fileName = state.filename || state.file?.opts?.filename || '<unknown>';
|
|
542
|
+
const lineInfo = loc ? `:${loc.start.line}:${loc.start.column}` : '';
|
|
543
|
+
console.warn(
|
|
544
|
+
`[what-compiler] Could not extract template for <${tagName}> at ${fileName}${lineInfo}. ` +
|
|
545
|
+
`Falling back to h() for this element. ` +
|
|
546
|
+
`This element could not be statically analyzed. Consider simplifying the JSX.`
|
|
547
|
+
);
|
|
548
|
+
state.needsH = true;
|
|
549
|
+
return transformElementAsH(path, state);
|
|
627
550
|
}
|
|
628
551
|
|
|
629
|
-
const tmplId =
|
|
630
|
-
state.templates.push({ id: tmplId, html });
|
|
552
|
+
const tmplId = getOrCreateTemplate(state, html);
|
|
631
553
|
state.needsTemplate = true;
|
|
632
554
|
|
|
633
555
|
const elId = state.nextVarId();
|
|
634
556
|
|
|
635
|
-
// _el$ =
|
|
557
|
+
// Build statements: _el$ = _tmpl$1()
|
|
558
|
+
// NO IIFE wrapping — statements are inlined into the containing function
|
|
636
559
|
const statements = [
|
|
637
560
|
t.variableDeclaration('const', [
|
|
638
561
|
t.variableDeclarator(t.identifier(elId), t.callExpression(t.identifier(tmplId), []))
|
|
@@ -645,13 +568,55 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
645
568
|
// Handle dynamic children
|
|
646
569
|
applyDynamicChildren(statements, elId, children, node, state);
|
|
647
570
|
|
|
648
|
-
//
|
|
649
|
-
|
|
571
|
+
// Instead of wrapping in an IIFE, store setup statements for hoisting.
|
|
572
|
+
// The JSXElement visitor will insert them before the enclosing statement.
|
|
573
|
+
if (!state._pendingSetup) state._pendingSetup = [];
|
|
574
|
+
state._pendingSetup.push(...statements);
|
|
575
|
+
return t.identifier(elId);
|
|
576
|
+
}
|
|
650
577
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
578
|
+
// Fallback: transform element using h() when template extraction fails
|
|
579
|
+
function transformElementAsH(path, state) {
|
|
580
|
+
const { node } = path;
|
|
581
|
+
const openingElement = node.openingElement;
|
|
582
|
+
const tagName = openingElement.name.name;
|
|
583
|
+
const attributes = openingElement.attributes;
|
|
584
|
+
const children = node.children;
|
|
585
|
+
|
|
586
|
+
const props = [];
|
|
587
|
+
for (const attr of attributes) {
|
|
588
|
+
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
589
|
+
const attrName = getAttrName(attr);
|
|
590
|
+
const value = getAttributeValue(attr.value);
|
|
591
|
+
let domAttrName = normalizeAttrName(attrName);
|
|
592
|
+
props.push(
|
|
593
|
+
t.objectProperty(
|
|
594
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(domAttrName)
|
|
595
|
+
? t.identifier(domAttrName)
|
|
596
|
+
: t.stringLiteral(domAttrName),
|
|
597
|
+
value
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const transformedChildren = [];
|
|
603
|
+
for (const child of children) {
|
|
604
|
+
if (t.isJSXText(child)) {
|
|
605
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
606
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
607
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
608
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
609
|
+
transformedChildren.push(child.expression);
|
|
610
|
+
}
|
|
611
|
+
} else if (t.isJSXElement(child)) {
|
|
612
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
613
|
+
} else if (t.isJSXFragment(child)) {
|
|
614
|
+
transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const propsExpr = props.length > 0 ? t.objectExpression(props) : t.nullLiteral();
|
|
619
|
+
return t.callExpression(t.identifier('h'), [t.stringLiteral(tagName), propsExpr, ...transformedChildren]);
|
|
655
620
|
}
|
|
656
621
|
|
|
657
622
|
function applyDynamicAttrs(statements, elId, attributes, state) {
|
|
@@ -666,7 +631,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
666
631
|
|
|
667
632
|
for (const attr of attributes) {
|
|
668
633
|
if (t.isJSXSpreadAttribute(attr)) {
|
|
669
|
-
// spread(el, props) — use runtime spread
|
|
670
634
|
state.needsSpread = true;
|
|
671
635
|
statements.push(
|
|
672
636
|
t.expressionStatement(
|
|
@@ -676,24 +640,65 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
676
640
|
continue;
|
|
677
641
|
}
|
|
678
642
|
|
|
679
|
-
const attrName = attr
|
|
643
|
+
const attrName = getAttrName(attr);
|
|
680
644
|
|
|
681
|
-
//
|
|
682
|
-
if (attrName
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
// Direct addEventListener
|
|
645
|
+
// Ref handling — assign element to ref object/callback
|
|
646
|
+
if (attrName === 'ref') {
|
|
647
|
+
const refExpr = getAttributeValue(attr.value);
|
|
648
|
+
// Generate: typeof ref === 'function' ? ref(el) : ref.current = el
|
|
686
649
|
statements.push(
|
|
687
650
|
t.expressionStatement(
|
|
688
|
-
t.
|
|
689
|
-
t.
|
|
690
|
-
|
|
651
|
+
t.conditionalExpression(
|
|
652
|
+
t.binaryExpression('===',
|
|
653
|
+
t.unaryExpression('typeof', refExpr),
|
|
654
|
+
t.stringLiteral('function')
|
|
655
|
+
),
|
|
656
|
+
t.callExpression(t.cloneNode(refExpr), [t.identifier(elId)]),
|
|
657
|
+
t.assignmentExpression('=',
|
|
658
|
+
t.memberExpression(t.cloneNode(refExpr), t.identifier('current')),
|
|
659
|
+
t.identifier(elId)
|
|
660
|
+
)
|
|
691
661
|
)
|
|
692
662
|
)
|
|
693
663
|
);
|
|
694
664
|
continue;
|
|
695
665
|
}
|
|
696
666
|
|
|
667
|
+
// Event handlers
|
|
668
|
+
if (attrName.startsWith('on') && !attrName.includes('|')) {
|
|
669
|
+
const event = attrName.slice(2).toLowerCase();
|
|
670
|
+
const handler = getAttributeValue(attr.value);
|
|
671
|
+
|
|
672
|
+
if (DELEGATED_EVENTS.has(event)) {
|
|
673
|
+
// Use event delegation: el.__click = handler
|
|
674
|
+
state.needsDelegation = true;
|
|
675
|
+
if (!state.delegatedEvents) state.delegatedEvents = new Set();
|
|
676
|
+
state.delegatedEvents.add(event);
|
|
677
|
+
statements.push(
|
|
678
|
+
t.expressionStatement(
|
|
679
|
+
t.assignmentExpression('=',
|
|
680
|
+
t.memberExpression(
|
|
681
|
+
t.identifier(elId),
|
|
682
|
+
t.identifier(`__${event}`)
|
|
683
|
+
),
|
|
684
|
+
handler
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
} else {
|
|
689
|
+
// Non-delegated: use per-element addEventListener
|
|
690
|
+
statements.push(
|
|
691
|
+
t.expressionStatement(
|
|
692
|
+
t.callExpression(
|
|
693
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
694
|
+
[t.stringLiteral(event), handler]
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
|
|
697
702
|
// Event with modifiers
|
|
698
703
|
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
699
704
|
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
@@ -728,7 +733,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
728
733
|
state.needsEffect = true;
|
|
729
734
|
|
|
730
735
|
if (bindProp === 'value') {
|
|
731
|
-
// Reactive value binding
|
|
732
736
|
statements.push(
|
|
733
737
|
t.expressionStatement(
|
|
734
738
|
t.callExpression(t.identifier('_$effect'), [
|
|
@@ -739,7 +743,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
739
743
|
])
|
|
740
744
|
)
|
|
741
745
|
);
|
|
742
|
-
// Input listener
|
|
743
746
|
statements.push(
|
|
744
747
|
t.expressionStatement(
|
|
745
748
|
t.callExpression(
|
|
@@ -760,6 +763,38 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
760
763
|
)
|
|
761
764
|
)
|
|
762
765
|
);
|
|
766
|
+
} else if (bindProp === 'checked') {
|
|
767
|
+
state.needsEffect = true;
|
|
768
|
+
statements.push(
|
|
769
|
+
t.expressionStatement(
|
|
770
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
771
|
+
t.arrowFunctionExpression([], t.assignmentExpression('=',
|
|
772
|
+
t.memberExpression(t.identifier(elId), t.identifier('checked')),
|
|
773
|
+
t.callExpression(t.cloneNode(signalExpr), [])
|
|
774
|
+
))
|
|
775
|
+
])
|
|
776
|
+
)
|
|
777
|
+
);
|
|
778
|
+
statements.push(
|
|
779
|
+
t.expressionStatement(
|
|
780
|
+
t.callExpression(
|
|
781
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
782
|
+
[
|
|
783
|
+
t.stringLiteral('change'),
|
|
784
|
+
t.arrowFunctionExpression(
|
|
785
|
+
[t.identifier('e')],
|
|
786
|
+
t.callExpression(
|
|
787
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
788
|
+
[t.memberExpression(
|
|
789
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
790
|
+
t.identifier('checked')
|
|
791
|
+
)]
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
]
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
);
|
|
763
798
|
}
|
|
764
799
|
continue;
|
|
765
800
|
}
|
|
@@ -769,28 +804,35 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
769
804
|
const expr = attr.value.expression;
|
|
770
805
|
const domName = normalizeAttrName(attrName);
|
|
771
806
|
|
|
772
|
-
if (isPotentiallyReactive(expr)) {
|
|
773
|
-
// Reactive attribute — wrap in effect
|
|
807
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
774
808
|
state.needsEffect = true;
|
|
775
|
-
|
|
776
|
-
t.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
809
|
+
const effectCall = t.callExpression(t.identifier('_$effect'), [
|
|
810
|
+
t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
|
|
811
|
+
]);
|
|
812
|
+
// In dev mode, add a leading comment when the effect wrapping is uncertain
|
|
813
|
+
// (non-signal function call whose args happen to contain signal reads)
|
|
814
|
+
if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
815
|
+
t.addComment(effectCall, 'leading',
|
|
816
|
+
' @what-dev: effect wrapping may be unnecessary — expression contains a non-signal function call with reactive args ',
|
|
817
|
+
false
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
statements.push(t.expressionStatement(effectCall));
|
|
782
821
|
} else {
|
|
783
822
|
// Static expression (no signal calls) — set once
|
|
784
823
|
statements.push(t.expressionStatement(buildSetPropCall(domName, expr)));
|
|
785
824
|
}
|
|
786
825
|
}
|
|
787
|
-
// Static string/boolean attributes already in template
|
|
788
826
|
}
|
|
789
827
|
}
|
|
790
828
|
|
|
791
829
|
function applyDynamicChildren(statements, elId, children, parentNode, state) {
|
|
792
|
-
//
|
|
793
|
-
//
|
|
830
|
+
// Two-pass approach: first collect all children needing DOM references,
|
|
831
|
+
// then pre-capture markers before any _$insert() calls shift indices.
|
|
832
|
+
// This fixes issue #1: childNodes index shifting with multiple dynamic children.
|
|
833
|
+
|
|
834
|
+
// --- Pass 1: Scan children and collect entries ---
|
|
835
|
+
const entries = [];
|
|
794
836
|
let childIndex = 0;
|
|
795
837
|
|
|
796
838
|
for (const child of children) {
|
|
@@ -802,21 +844,88 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
802
844
|
|
|
803
845
|
if (t.isJSXExpressionContainer(child)) {
|
|
804
846
|
if (t.isJSXEmptyExpression(child.expression)) continue;
|
|
847
|
+
entries.push({ type: 'expression', child, childIndex });
|
|
848
|
+
childIndex++;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
805
851
|
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
852
|
+
if (t.isJSXElement(child)) {
|
|
853
|
+
const childTag = child.openingElement.name.name;
|
|
854
|
+
if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
|
|
855
|
+
entries.push({ type: 'component', child, childIndex });
|
|
856
|
+
childIndex++;
|
|
857
|
+
} else {
|
|
858
|
+
const hasAnythingDynamic = child.openingElement.attributes.some(isDynamicAttr) ||
|
|
859
|
+
child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && getAttrName(a)?.startsWith('on')) ||
|
|
860
|
+
!child.children.every(isStaticChild);
|
|
809
861
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
862
|
+
entries.push({ type: 'static', child, childIndex, hasAnythingDynamic });
|
|
863
|
+
childIndex++;
|
|
864
|
+
}
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (t.isJSXFragment(child)) {
|
|
869
|
+
entries.push({ type: 'fragment', child });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// --- Pre-capture marker references if needed ---
|
|
874
|
+
// When there are multiple entries needing DOM refs and at least one _$insert(),
|
|
875
|
+
// capture all markers upfront to avoid index shifting after DOM mutations.
|
|
876
|
+
const entriesNeedingRef = entries.filter(e =>
|
|
877
|
+
e.type === 'expression' || e.type === 'component' ||
|
|
878
|
+
(e.type === 'static' && e.hasAnythingDynamic)
|
|
879
|
+
);
|
|
880
|
+
const hasDynamicInsert = entries.some(e => e.type === 'expression' || e.type === 'component');
|
|
881
|
+
const needsPreCapture = entriesNeedingRef.length >= 2 && hasDynamicInsert;
|
|
882
|
+
|
|
883
|
+
const markerVars = new Map(); // childIndex → variable name
|
|
884
|
+
if (needsPreCapture) {
|
|
885
|
+
for (const entry of entriesNeedingRef) {
|
|
886
|
+
const varName = `_m$${entry.childIndex}`;
|
|
887
|
+
// Use a unique name to avoid collisions with element vars
|
|
888
|
+
const markerVar = state.nextVarId();
|
|
889
|
+
markerVars.set(entry.childIndex, markerVar);
|
|
890
|
+
statements.push(
|
|
891
|
+
t.variableDeclaration('const', [
|
|
892
|
+
t.variableDeclarator(
|
|
893
|
+
t.identifier(markerVar),
|
|
894
|
+
buildChildAccess(elId, entry.childIndex)
|
|
818
895
|
)
|
|
819
|
-
)
|
|
896
|
+
])
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Helper: get a marker reference (pre-captured var or inline access)
|
|
902
|
+
function getMarker(idx) {
|
|
903
|
+
if (markerVars.has(idx)) {
|
|
904
|
+
return t.identifier(markerVars.get(idx));
|
|
905
|
+
}
|
|
906
|
+
return buildChildAccess(elId, idx);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// --- Pass 2: Generate code using stable references ---
|
|
910
|
+
for (const entry of entries) {
|
|
911
|
+
if (entry.type === 'expression') {
|
|
912
|
+
const expr = entry.child.expression;
|
|
913
|
+
const marker = getMarker(entry.childIndex);
|
|
914
|
+
state.needsInsert = true;
|
|
915
|
+
|
|
916
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
917
|
+
const insertCall = t.callExpression(t.identifier('_$insert'), [
|
|
918
|
+
t.identifier(elId),
|
|
919
|
+
t.arrowFunctionExpression([], expr),
|
|
920
|
+
marker
|
|
921
|
+
]);
|
|
922
|
+
if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
923
|
+
t.addComment(insertCall, 'leading',
|
|
924
|
+
' @what-dev: reactive wrapping may be unnecessary — expression contains a non-signal function call with reactive args ',
|
|
925
|
+
false
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
statements.push(t.expressionStatement(insertCall));
|
|
820
929
|
} else {
|
|
821
930
|
statements.push(
|
|
822
931
|
t.expressionStatement(
|
|
@@ -828,60 +937,52 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
828
937
|
)
|
|
829
938
|
);
|
|
830
939
|
}
|
|
831
|
-
childIndex++;
|
|
832
940
|
continue;
|
|
833
941
|
}
|
|
834
942
|
|
|
835
|
-
if (
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
943
|
+
if (entry.type === 'component') {
|
|
944
|
+
const transformed = transformElementFineGrained({ node: entry.child }, state);
|
|
945
|
+
const marker = getMarker(entry.childIndex);
|
|
946
|
+
state.needsInsert = true;
|
|
947
|
+
statements.push(
|
|
948
|
+
t.expressionStatement(
|
|
949
|
+
t.callExpression(t.identifier('_$insert'), [
|
|
950
|
+
t.identifier(elId),
|
|
951
|
+
transformed,
|
|
952
|
+
marker
|
|
953
|
+
])
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (entry.type === 'static' && entry.hasAnythingDynamic) {
|
|
960
|
+
// Static child with dynamic content — get element reference
|
|
961
|
+
let childElRef;
|
|
962
|
+
if (markerVars.has(entry.childIndex)) {
|
|
963
|
+
childElRef = markerVars.get(entry.childIndex);
|
|
964
|
+
} else {
|
|
965
|
+
childElRef = state.nextVarId();
|
|
842
966
|
statements.push(
|
|
843
|
-
t.
|
|
844
|
-
t.
|
|
845
|
-
t.identifier(
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
)
|
|
967
|
+
t.variableDeclaration('const', [
|
|
968
|
+
t.variableDeclarator(
|
|
969
|
+
t.identifier(childElRef),
|
|
970
|
+
buildChildAccess(elId, entry.childIndex)
|
|
971
|
+
)
|
|
972
|
+
])
|
|
850
973
|
);
|
|
851
|
-
childIndex++;
|
|
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
974
|
}
|
|
975
|
+
applyDynamicAttrs(statements, childElRef, entry.child.openingElement.attributes, state);
|
|
976
|
+
applyDynamicChildren(statements, childElRef, entry.child.children, entry.child, state);
|
|
875
977
|
continue;
|
|
876
978
|
}
|
|
877
979
|
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
for (const fChild of child.children) {
|
|
980
|
+
if (entry.type === 'fragment') {
|
|
981
|
+
for (const fChild of entry.child.children) {
|
|
881
982
|
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
882
983
|
state.needsInsert = true;
|
|
883
984
|
const expr = fChild.expression;
|
|
884
|
-
if (isPotentiallyReactive(expr)) {
|
|
985
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
885
986
|
statements.push(
|
|
886
987
|
t.expressionStatement(
|
|
887
988
|
t.callExpression(t.identifier('_$insert'), [
|
|
@@ -907,19 +1008,202 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
907
1008
|
}
|
|
908
1009
|
|
|
909
1010
|
function buildChildAccess(elId, index) {
|
|
910
|
-
// Use
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
t.memberExpression(t.identifier(elId), t.identifier('
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
);
|
|
1011
|
+
// Use firstChild/nextSibling chains instead of childNodes[N]
|
|
1012
|
+
// This is more robust with whitespace text nodes
|
|
1013
|
+
if (index === 0) {
|
|
1014
|
+
return t.memberExpression(t.identifier(elId), t.identifier('firstChild'));
|
|
1015
|
+
}
|
|
1016
|
+
// Chain .nextSibling for subsequent indices
|
|
1017
|
+
let expr = t.memberExpression(t.identifier(elId), t.identifier('firstChild'));
|
|
1018
|
+
for (let i = 0; i < index; i++) {
|
|
1019
|
+
expr = t.memberExpression(expr, t.identifier('nextSibling'));
|
|
1020
|
+
}
|
|
1021
|
+
return expr;
|
|
917
1022
|
}
|
|
918
1023
|
|
|
919
1024
|
function transformComponentFineGrained(path, state) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1025
|
+
const { node } = path;
|
|
1026
|
+
const openingElement = node.openingElement;
|
|
1027
|
+
const componentName = openingElement.name.name;
|
|
1028
|
+
const attributes = openingElement.attributes;
|
|
1029
|
+
const children = node.children;
|
|
1030
|
+
|
|
1031
|
+
// Check for client: directive (islands)
|
|
1032
|
+
let clientDirective = null;
|
|
1033
|
+
const filteredAttrs = [];
|
|
1034
|
+
|
|
1035
|
+
for (const attr of attributes) {
|
|
1036
|
+
if (t.isJSXAttribute(attr)) {
|
|
1037
|
+
// Handle both simple names and namespaced names (client:idle)
|
|
1038
|
+
let name;
|
|
1039
|
+
if (t.isJSXNamespacedName(attr.name)) {
|
|
1040
|
+
name = `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
1041
|
+
} else {
|
|
1042
|
+
name = attr.name.name;
|
|
1043
|
+
}
|
|
1044
|
+
if (name && typeof name === 'string' && name.startsWith('client:')) {
|
|
1045
|
+
const mode = name.slice(7);
|
|
1046
|
+
if (attr.value) {
|
|
1047
|
+
clientDirective = { type: mode, value: attr.value.value };
|
|
1048
|
+
} else {
|
|
1049
|
+
clientDirective = { type: mode };
|
|
1050
|
+
}
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
filteredAttrs.push(attr);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (clientDirective) {
|
|
1058
|
+
state.needsCreateComponent = true;
|
|
1059
|
+
state.needsIsland = true;
|
|
1060
|
+
|
|
1061
|
+
const islandProps = [
|
|
1062
|
+
t.objectProperty(t.identifier('component'), t.identifier(componentName)),
|
|
1063
|
+
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
1064
|
+
];
|
|
1065
|
+
|
|
1066
|
+
if (clientDirective.value) {
|
|
1067
|
+
islandProps.push(
|
|
1068
|
+
t.objectProperty(t.identifier('mediaQuery'), t.stringLiteral(clientDirective.value))
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
for (const attr of filteredAttrs) {
|
|
1073
|
+
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
1074
|
+
const attrName = getAttrName(attr);
|
|
1075
|
+
const value = getAttributeValue(attr.value);
|
|
1076
|
+
islandProps.push(t.objectProperty(t.identifier(attrName), value));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return t.callExpression(
|
|
1080
|
+
t.identifier('_$createComponent'),
|
|
1081
|
+
[t.identifier('Island'), t.objectExpression(islandProps), t.arrayExpression([])]
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Regular component — use _$createComponent to instantiate, component runs once
|
|
1086
|
+
state.needsCreateComponent = true;
|
|
1087
|
+
|
|
1088
|
+
const props = [];
|
|
1089
|
+
let hasSpread = false;
|
|
1090
|
+
let spreadExpr = null;
|
|
1091
|
+
|
|
1092
|
+
for (const attr of filteredAttrs) {
|
|
1093
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
1094
|
+
hasSpread = true;
|
|
1095
|
+
spreadExpr = attr.argument;
|
|
1096
|
+
continue;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const attrName = getAttrName(attr);
|
|
1100
|
+
|
|
1101
|
+
// Handle bind: attributes for components
|
|
1102
|
+
if (isBindingAttribute(attrName)) {
|
|
1103
|
+
const bindProp = getBindingProperty(attrName);
|
|
1104
|
+
const signalExpr = attr.value.expression;
|
|
1105
|
+
|
|
1106
|
+
if (bindProp === 'value') {
|
|
1107
|
+
props.push(
|
|
1108
|
+
t.objectProperty(t.identifier('value'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
1109
|
+
);
|
|
1110
|
+
props.push(
|
|
1111
|
+
t.objectProperty(
|
|
1112
|
+
t.identifier('onInput'),
|
|
1113
|
+
t.arrowFunctionExpression(
|
|
1114
|
+
[t.identifier('e')],
|
|
1115
|
+
t.callExpression(
|
|
1116
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
1117
|
+
[t.memberExpression(
|
|
1118
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
1119
|
+
t.identifier('value')
|
|
1120
|
+
)]
|
|
1121
|
+
)
|
|
1122
|
+
)
|
|
1123
|
+
)
|
|
1124
|
+
);
|
|
1125
|
+
} else if (bindProp === 'checked') {
|
|
1126
|
+
props.push(
|
|
1127
|
+
t.objectProperty(t.identifier('checked'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
1128
|
+
);
|
|
1129
|
+
props.push(
|
|
1130
|
+
t.objectProperty(
|
|
1131
|
+
t.identifier('onChange'),
|
|
1132
|
+
t.arrowFunctionExpression(
|
|
1133
|
+
[t.identifier('e')],
|
|
1134
|
+
t.callExpression(
|
|
1135
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
1136
|
+
[t.memberExpression(
|
|
1137
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
1138
|
+
t.identifier('checked')
|
|
1139
|
+
)]
|
|
1140
|
+
)
|
|
1141
|
+
)
|
|
1142
|
+
)
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Handle event modifiers on components
|
|
1149
|
+
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
1150
|
+
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
1151
|
+
const handler = getAttributeValue(attr.value);
|
|
1152
|
+
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
1153
|
+
props.push(t.objectProperty(t.identifier(eventName), wrappedHandler));
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const value = getAttributeValue(attr.value);
|
|
1158
|
+
|
|
1159
|
+
props.push(
|
|
1160
|
+
t.objectProperty(
|
|
1161
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(attrName)
|
|
1162
|
+
? t.identifier(attrName)
|
|
1163
|
+
: t.stringLiteral(attrName),
|
|
1164
|
+
value
|
|
1165
|
+
)
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Transform children
|
|
1170
|
+
const transformedChildren = [];
|
|
1171
|
+
for (const child of children) {
|
|
1172
|
+
if (t.isJSXText(child)) {
|
|
1173
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
1174
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1175
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
1176
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1177
|
+
transformedChildren.push(child.expression);
|
|
1178
|
+
}
|
|
1179
|
+
} else if (t.isJSXElement(child)) {
|
|
1180
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1181
|
+
} else if (t.isJSXFragment(child)) {
|
|
1182
|
+
transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
let propsExpr;
|
|
1187
|
+
if (hasSpread) {
|
|
1188
|
+
if (props.length > 0) {
|
|
1189
|
+
propsExpr = t.callExpression(
|
|
1190
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
1191
|
+
[t.objectExpression([]), spreadExpr, t.objectExpression(props)]
|
|
1192
|
+
);
|
|
1193
|
+
} else {
|
|
1194
|
+
propsExpr = spreadExpr;
|
|
1195
|
+
}
|
|
1196
|
+
} else if (props.length > 0) {
|
|
1197
|
+
propsExpr = t.objectExpression(props);
|
|
1198
|
+
} else {
|
|
1199
|
+
propsExpr = t.nullLiteral();
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const childrenArray = transformedChildren.length > 0
|
|
1203
|
+
? t.arrayExpression(transformedChildren)
|
|
1204
|
+
: t.arrayExpression([]);
|
|
1205
|
+
|
|
1206
|
+
return t.callExpression(t.identifier('_$createComponent'), [t.identifier(componentName), propsExpr, childrenArray]);
|
|
923
1207
|
}
|
|
924
1208
|
|
|
925
1209
|
function transformForFineGrained(path, state) {
|
|
@@ -931,17 +1215,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
931
1215
|
// → mapArray(data, (item) => ...)
|
|
932
1216
|
let eachExpr = null;
|
|
933
1217
|
for (const attr of attributes) {
|
|
934
|
-
if (t.isJSXAttribute(attr) && attr
|
|
1218
|
+
if (t.isJSXAttribute(attr) && getAttrName(attr) === 'each') {
|
|
935
1219
|
eachExpr = getAttributeValue(attr.value);
|
|
936
1220
|
}
|
|
937
1221
|
}
|
|
938
1222
|
|
|
939
1223
|
if (!eachExpr) {
|
|
940
|
-
|
|
941
|
-
|
|
1224
|
+
console.warn('[what-compiler] <For> element missing "each" attribute.');
|
|
1225
|
+
state.needsH = true;
|
|
1226
|
+
return transformElementAsH(path, state);
|
|
942
1227
|
}
|
|
943
1228
|
|
|
944
|
-
// Get the render function from children
|
|
945
1229
|
let renderFn = null;
|
|
946
1230
|
for (const child of children) {
|
|
947
1231
|
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -951,7 +1235,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
951
1235
|
}
|
|
952
1236
|
|
|
953
1237
|
if (!renderFn) {
|
|
954
|
-
|
|
1238
|
+
console.warn('[what-compiler] <For> element missing render function child.');
|
|
1239
|
+
state.needsH = true;
|
|
1240
|
+
return transformElementAsH(path, state);
|
|
955
1241
|
}
|
|
956
1242
|
|
|
957
1243
|
state.needsMapArray = true;
|
|
@@ -959,20 +1245,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
959
1245
|
}
|
|
960
1246
|
|
|
961
1247
|
function transformShowFineGrained(path, state) {
|
|
962
|
-
const { node } = path;
|
|
963
|
-
const attributes = node.openingElement.attributes;
|
|
964
|
-
const children = node.children;
|
|
965
|
-
|
|
966
1248
|
// <Show when={cond}>{content}</Show>
|
|
967
|
-
//
|
|
968
|
-
|
|
1249
|
+
// Uses _$createComponent(Show, ...) — Show is a runtime component
|
|
1250
|
+
state.needsCreateComponent = true;
|
|
1251
|
+
return transformComponentFineGrained(path, state);
|
|
969
1252
|
}
|
|
970
1253
|
|
|
971
1254
|
function transformFragmentFineGrained(path, state) {
|
|
972
1255
|
const { node } = path;
|
|
973
1256
|
const children = node.children;
|
|
974
1257
|
|
|
975
|
-
// Fragments with fine-grained: just return children array or single child
|
|
976
1258
|
const transformed = [];
|
|
977
1259
|
for (const child of children) {
|
|
978
1260
|
if (t.isJSXText(child)) {
|
|
@@ -993,8 +1275,15 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
993
1275
|
return t.arrayExpression(transformed);
|
|
994
1276
|
}
|
|
995
1277
|
|
|
996
|
-
|
|
997
|
-
|
|
1278
|
+
// Template deduplication: same HTML string → same module-level const
|
|
1279
|
+
function getOrCreateTemplate(state, html) {
|
|
1280
|
+
if (state.templateMap.has(html)) {
|
|
1281
|
+
return state.templateMap.get(html);
|
|
1282
|
+
}
|
|
1283
|
+
const id = `_tmpl$${state.templateCount++}`;
|
|
1284
|
+
state.templateMap.set(html, id);
|
|
1285
|
+
state.templates.push({ id, html });
|
|
1286
|
+
return id;
|
|
998
1287
|
}
|
|
999
1288
|
|
|
1000
1289
|
// =====================================================
|
|
@@ -1007,15 +1296,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1007
1296
|
visitor: {
|
|
1008
1297
|
Program: {
|
|
1009
1298
|
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
|
|
1015
|
-
state.needsH = false;
|
|
1016
|
-
state.needsFragment = false;
|
|
1017
|
-
state.needsIsland = false;
|
|
1018
|
-
|
|
1019
1299
|
// Fine-grained mode state
|
|
1020
1300
|
state.needsTemplate = false;
|
|
1021
1301
|
state.needsInsert = false;
|
|
@@ -1023,147 +1303,252 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1023
1303
|
state.needsMapArray = false;
|
|
1024
1304
|
state.needsSpread = false;
|
|
1025
1305
|
state.needsSetProp = false;
|
|
1306
|
+
state.needsH = false;
|
|
1307
|
+
state.needsCreateComponent = false;
|
|
1308
|
+
state.needsFragment = false;
|
|
1309
|
+
state.needsIsland = false;
|
|
1310
|
+
state.needsDelegation = false;
|
|
1311
|
+
state.delegatedEvents = new Set();
|
|
1026
1312
|
state.templates = [];
|
|
1313
|
+
state.templateMap = new Map(); // html → template id (deduplication)
|
|
1027
1314
|
state.templateCount = 0;
|
|
1028
1315
|
state._varCounter = 0;
|
|
1316
|
+
state._pendingSetup = [];
|
|
1029
1317
|
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
1030
|
-
},
|
|
1031
|
-
|
|
1032
|
-
exit(path, state) {
|
|
1033
|
-
if (state.mode === 'fine-grained') {
|
|
1034
|
-
// Insert template declarations at top of program
|
|
1035
|
-
for (const tmpl of state.templates.reverse()) {
|
|
1036
|
-
path.unshiftContainer('body',
|
|
1037
|
-
t.variableDeclaration('const', [
|
|
1038
|
-
t.variableDeclarator(
|
|
1039
|
-
t.identifier(tmpl.id),
|
|
1040
|
-
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1041
|
-
)
|
|
1042
|
-
])
|
|
1043
|
-
);
|
|
1044
|
-
}
|
|
1045
1318
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
)
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Also include h/Fragment/Island if vdom mode used for components
|
|
1080
|
-
const coreSpecifiers = [];
|
|
1081
|
-
if (state.needsH) {
|
|
1082
|
-
coreSpecifiers.push(
|
|
1083
|
-
t.importSpecifier(t.identifier('h'), t.identifier('h'))
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
if (state.needsFragment) {
|
|
1087
|
-
coreSpecifiers.push(
|
|
1088
|
-
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
if (state.needsIsland) {
|
|
1092
|
-
coreSpecifiers.push(
|
|
1093
|
-
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
1094
|
-
);
|
|
1095
|
-
}
|
|
1319
|
+
// Collect signal names for smart reactivity detection
|
|
1320
|
+
state.signalNames = new Set();
|
|
1321
|
+
|
|
1322
|
+
// --- Imported Signal Tracking ---
|
|
1323
|
+
// Only mark imports as potentially reactive if they come from known
|
|
1324
|
+
// reactive sources: what-framework, what-framework/*, relative paths
|
|
1325
|
+
// (user stores), or functions matching use*/create* naming conventions.
|
|
1326
|
+
// This prevents over-wrapping of utility imports (lodash, etc.).
|
|
1327
|
+
state.importedIdentifiers = new Set();
|
|
1328
|
+
for (const node of path.node.body) {
|
|
1329
|
+
if (t.isImportDeclaration(node)) {
|
|
1330
|
+
const source = node.source.value;
|
|
1331
|
+
const isReactiveSource =
|
|
1332
|
+
source === 'what-framework' ||
|
|
1333
|
+
source.startsWith('what-framework/') ||
|
|
1334
|
+
source === 'what-core' ||
|
|
1335
|
+
source.startsWith('what-core/') ||
|
|
1336
|
+
source.startsWith('./') ||
|
|
1337
|
+
source.startsWith('../');
|
|
1338
|
+
|
|
1339
|
+
for (const spec of node.specifiers) {
|
|
1340
|
+
let localName = null;
|
|
1341
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1342
|
+
localName = spec.local.name;
|
|
1343
|
+
} else if (t.isImportDefaultSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1344
|
+
localName = spec.local.name;
|
|
1345
|
+
} else if (t.isImportNamespaceSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1346
|
+
localName = spec.local.name;
|
|
1347
|
+
}
|
|
1096
1348
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
node.source.value === 'what-core/render'
|
|
1104
|
-
)) {
|
|
1105
|
-
existingRenderImport = node;
|
|
1106
|
-
break;
|
|
1349
|
+
if (localName) {
|
|
1350
|
+
// Mark as reactive if from a reactive source, or if the name
|
|
1351
|
+
// matches use*/create* conventions (hooks/signal creators)
|
|
1352
|
+
if (isReactiveSource || /^(use|create)[A-Z]/.test(localName)) {
|
|
1353
|
+
state.importedIdentifiers.add(localName);
|
|
1354
|
+
}
|
|
1107
1355
|
}
|
|
1108
1356
|
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1109
1359
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1360
|
+
path.traverse({
|
|
1361
|
+
VariableDeclarator(declPath) {
|
|
1362
|
+
const init = declPath.node.init;
|
|
1363
|
+
if (!init || !t.isCallExpression(init)) return;
|
|
1364
|
+
|
|
1365
|
+
const callee = init.callee;
|
|
1366
|
+
let calleeName = '';
|
|
1367
|
+
if (t.isIdentifier(callee)) {
|
|
1368
|
+
calleeName = callee.name;
|
|
1369
|
+
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
1370
|
+
calleeName = callee.property.name;
|
|
1371
|
+
}
|
|
1116
1372
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1373
|
+
if (SIGNAL_CREATORS.has(calleeName)) {
|
|
1374
|
+
const id = declPath.node.id;
|
|
1375
|
+
if (t.isIdentifier(id)) {
|
|
1376
|
+
state.signalNames.add(id.name);
|
|
1377
|
+
} else if (t.isArrayPattern(id)) {
|
|
1378
|
+
for (const el of id.elements) {
|
|
1379
|
+
if (t.isIdentifier(el)) state.signalNames.add(el.name);
|
|
1380
|
+
}
|
|
1381
|
+
} else if (t.isObjectPattern(id)) {
|
|
1382
|
+
for (const prop of id.properties) {
|
|
1383
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
1384
|
+
state.signalNames.add(prop.value.name);
|
|
1385
|
+
}
|
|
1120
1386
|
}
|
|
1121
1387
|
}
|
|
1122
|
-
} else {
|
|
1123
|
-
path.unshiftContainer('body',
|
|
1124
|
-
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1125
|
-
);
|
|
1126
1388
|
}
|
|
1127
1389
|
}
|
|
1390
|
+
});
|
|
1391
|
+
},
|
|
1392
|
+
|
|
1393
|
+
exit(path, state) {
|
|
1394
|
+
// Insert template declarations at top of program (hoisted to module scope)
|
|
1395
|
+
for (const tmpl of state.templates.reverse()) {
|
|
1396
|
+
path.unshiftContainer('body',
|
|
1397
|
+
t.variableDeclaration('const', [
|
|
1398
|
+
t.variableDeclarator(
|
|
1399
|
+
t.identifier(tmpl.id),
|
|
1400
|
+
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1401
|
+
)
|
|
1402
|
+
])
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Build fine-grained imports
|
|
1407
|
+
const fgSpecifiers = [];
|
|
1408
|
+
if (state.needsTemplate) {
|
|
1409
|
+
fgSpecifiers.push(
|
|
1410
|
+
t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (state.needsInsert) {
|
|
1414
|
+
fgSpecifiers.push(
|
|
1415
|
+
t.importSpecifier(t.identifier('_$insert'), t.identifier('insert'))
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
if (state.needsEffect) {
|
|
1419
|
+
fgSpecifiers.push(
|
|
1420
|
+
t.importSpecifier(t.identifier('_$effect'), t.identifier('effect'))
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
if (state.needsMapArray) {
|
|
1424
|
+
fgSpecifiers.push(
|
|
1425
|
+
t.importSpecifier(t.identifier('_$mapArray'), t.identifier('mapArray'))
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
if (state.needsSpread) {
|
|
1429
|
+
fgSpecifiers.push(
|
|
1430
|
+
t.importSpecifier(t.identifier('_$spread'), t.identifier('spread'))
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
if (state.needsSetProp) {
|
|
1434
|
+
fgSpecifiers.push(
|
|
1435
|
+
t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
if (state.needsCreateComponent) {
|
|
1439
|
+
fgSpecifiers.push(
|
|
1440
|
+
t.importSpecifier(t.identifier('_$createComponent'), t.identifier('_$createComponent'))
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
if (state.needsDelegation) {
|
|
1444
|
+
fgSpecifiers.push(
|
|
1445
|
+
t.importSpecifier(t.identifier('_$delegateEvents'), t.identifier('delegateEvents'))
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Core imports (h/Fragment/Island for components)
|
|
1450
|
+
const coreSpecifiers = [];
|
|
1451
|
+
if (state.needsH) {
|
|
1452
|
+
coreSpecifiers.push(
|
|
1453
|
+
t.importSpecifier(t.identifier('h'), t.identifier('h'))
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (state.needsFragment) {
|
|
1457
|
+
coreSpecifiers.push(
|
|
1458
|
+
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
if (state.needsIsland) {
|
|
1462
|
+
coreSpecifiers.push(
|
|
1463
|
+
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1128
1466
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1467
|
+
if (fgSpecifiers.length > 0) {
|
|
1468
|
+
let existingRenderImport = null;
|
|
1469
|
+
for (const node of path.node.body) {
|
|
1470
|
+
if (t.isImportDeclaration(node) && (
|
|
1471
|
+
node.source.value === 'what-framework/render' ||
|
|
1472
|
+
node.source.value === 'what-core/render'
|
|
1473
|
+
)) {
|
|
1474
|
+
existingRenderImport = node;
|
|
1475
|
+
break;
|
|
1476
|
+
}
|
|
1131
1477
|
}
|
|
1132
1478
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
t.importSpecifier(t.identifier('h'), t.identifier('h')),
|
|
1139
|
-
];
|
|
1140
|
-
if (state.needsFragment) {
|
|
1141
|
-
coreSpecifiers.push(
|
|
1142
|
-
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1479
|
+
if (existingRenderImport) {
|
|
1480
|
+
const existingNames = new Set(
|
|
1481
|
+
existingRenderImport.specifiers
|
|
1482
|
+
.filter(s => t.isImportSpecifier(s))
|
|
1483
|
+
.map(s => s.imported.name)
|
|
1143
1484
|
);
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1485
|
+
for (const spec of fgSpecifiers) {
|
|
1486
|
+
if (!existingNames.has(spec.imported.name)) {
|
|
1487
|
+
existingRenderImport.specifiers.push(spec);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
} else {
|
|
1491
|
+
path.unshiftContainer('body',
|
|
1492
|
+
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1148
1493
|
);
|
|
1149
1494
|
}
|
|
1495
|
+
}
|
|
1150
1496
|
|
|
1497
|
+
if (coreSpecifiers.length > 0) {
|
|
1151
1498
|
addCoreImports(path, t, coreSpecifiers);
|
|
1152
1499
|
}
|
|
1500
|
+
|
|
1501
|
+
// Emit event delegation setup call if any delegated events were used
|
|
1502
|
+
if (state.needsDelegation && state.delegatedEvents && state.delegatedEvents.size > 0) {
|
|
1503
|
+
const eventArray = t.arrayExpression(
|
|
1504
|
+
[...state.delegatedEvents].map(e => t.stringLiteral(e))
|
|
1505
|
+
);
|
|
1506
|
+
path.pushContainer('body',
|
|
1507
|
+
t.expressionStatement(
|
|
1508
|
+
t.callExpression(t.identifier('_$delegateEvents'), [eventArray])
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1153
1512
|
}
|
|
1154
1513
|
},
|
|
1155
1514
|
|
|
1156
1515
|
JSXElement(path, state) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
path
|
|
1516
|
+
// FIX-1: Use scope-aware signal detection instead of file-global
|
|
1517
|
+
state.signalNames = collectSignalNamesFromScope(path);
|
|
1518
|
+
state._pendingSetup = [];
|
|
1519
|
+
const transformed = transformElementFineGrained(path, state);
|
|
1520
|
+
const pending = state._pendingSetup;
|
|
1521
|
+
state._pendingSetup = [];
|
|
1522
|
+
|
|
1523
|
+
if (pending.length > 0) {
|
|
1524
|
+
// Find the enclosing statement to hoist setup before it
|
|
1525
|
+
let stmtPath = path;
|
|
1526
|
+
while (stmtPath && !stmtPath.isStatement()) {
|
|
1527
|
+
stmtPath = stmtPath.parentPath;
|
|
1528
|
+
}
|
|
1529
|
+
if (stmtPath && stmtPath.isStatement()) {
|
|
1530
|
+
// Insert setup statements before the enclosing statement
|
|
1531
|
+
for (const stmt of pending) {
|
|
1532
|
+
stmtPath.insertBefore(stmt);
|
|
1533
|
+
}
|
|
1534
|
+
path.replaceWith(transformed);
|
|
1535
|
+
} else {
|
|
1536
|
+
// Fallback: if we can't find a statement parent, use IIFE
|
|
1537
|
+
pending.push(t.returnStatement(transformed));
|
|
1538
|
+
path.replaceWith(
|
|
1539
|
+
t.callExpression(
|
|
1540
|
+
t.arrowFunctionExpression([], t.blockStatement(pending)),
|
|
1541
|
+
[]
|
|
1542
|
+
)
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
} else {
|
|
1546
|
+
path.replaceWith(transformed);
|
|
1547
|
+
}
|
|
1161
1548
|
},
|
|
1162
1549
|
|
|
1163
1550
|
JSXFragment(path, state) {
|
|
1164
|
-
const transformed = state
|
|
1165
|
-
? transformFragmentFineGrained(path, state)
|
|
1166
|
-
: transformFragmentVdom(path, state);
|
|
1551
|
+
const transformed = transformFragmentFineGrained(path, state);
|
|
1167
1552
|
path.replaceWith(transformed);
|
|
1168
1553
|
}
|
|
1169
1554
|
}
|
|
@@ -1187,7 +1572,6 @@ function addCoreImports(path, t, coreSpecifiers) {
|
|
|
1187
1572
|
.filter(s => t.isImportSpecifier(s))
|
|
1188
1573
|
.map(s => s.imported.name)
|
|
1189
1574
|
);
|
|
1190
|
-
|
|
1191
1575
|
for (const spec of coreSpecifiers) {
|
|
1192
1576
|
if (!existingNames.has(spec.imported.name)) {
|
|
1193
1577
|
existingImport.specifiers.push(spec);
|