what-compiler 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/babel-plugin.js +1238 -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 +1996 -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 +1985 -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 +27 -11
- package/src/babel-plugin.js +818 -520
- 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,21 +640,40 @@ 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
645
|
// Event handlers
|
|
682
646
|
if (attrName.startsWith('on') && !attrName.includes('|')) {
|
|
683
647
|
const event = attrName.slice(2).toLowerCase();
|
|
684
648
|
const handler = getAttributeValue(attr.value);
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
649
|
+
|
|
650
|
+
if (DELEGATED_EVENTS.has(event)) {
|
|
651
|
+
// Use event delegation: el.__click = handler
|
|
652
|
+
state.needsDelegation = true;
|
|
653
|
+
if (!state.delegatedEvents) state.delegatedEvents = new Set();
|
|
654
|
+
state.delegatedEvents.add(event);
|
|
655
|
+
statements.push(
|
|
656
|
+
t.expressionStatement(
|
|
657
|
+
t.assignmentExpression('=',
|
|
658
|
+
t.memberExpression(
|
|
659
|
+
t.identifier(elId),
|
|
660
|
+
t.identifier(`__${event}`)
|
|
661
|
+
),
|
|
662
|
+
handler
|
|
663
|
+
)
|
|
691
664
|
)
|
|
692
|
-
)
|
|
693
|
-
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
// Non-delegated: use per-element addEventListener
|
|
668
|
+
statements.push(
|
|
669
|
+
t.expressionStatement(
|
|
670
|
+
t.callExpression(
|
|
671
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
672
|
+
[t.stringLiteral(event), handler]
|
|
673
|
+
)
|
|
674
|
+
)
|
|
675
|
+
);
|
|
676
|
+
}
|
|
694
677
|
continue;
|
|
695
678
|
}
|
|
696
679
|
|
|
@@ -728,7 +711,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
728
711
|
state.needsEffect = true;
|
|
729
712
|
|
|
730
713
|
if (bindProp === 'value') {
|
|
731
|
-
// Reactive value binding
|
|
732
714
|
statements.push(
|
|
733
715
|
t.expressionStatement(
|
|
734
716
|
t.callExpression(t.identifier('_$effect'), [
|
|
@@ -739,7 +721,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
739
721
|
])
|
|
740
722
|
)
|
|
741
723
|
);
|
|
742
|
-
// Input listener
|
|
743
724
|
statements.push(
|
|
744
725
|
t.expressionStatement(
|
|
745
726
|
t.callExpression(
|
|
@@ -760,6 +741,38 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
760
741
|
)
|
|
761
742
|
)
|
|
762
743
|
);
|
|
744
|
+
} else if (bindProp === 'checked') {
|
|
745
|
+
state.needsEffect = true;
|
|
746
|
+
statements.push(
|
|
747
|
+
t.expressionStatement(
|
|
748
|
+
t.callExpression(t.identifier('_$effect'), [
|
|
749
|
+
t.arrowFunctionExpression([], t.assignmentExpression('=',
|
|
750
|
+
t.memberExpression(t.identifier(elId), t.identifier('checked')),
|
|
751
|
+
t.callExpression(t.cloneNode(signalExpr), [])
|
|
752
|
+
))
|
|
753
|
+
])
|
|
754
|
+
)
|
|
755
|
+
);
|
|
756
|
+
statements.push(
|
|
757
|
+
t.expressionStatement(
|
|
758
|
+
t.callExpression(
|
|
759
|
+
t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
|
|
760
|
+
[
|
|
761
|
+
t.stringLiteral('change'),
|
|
762
|
+
t.arrowFunctionExpression(
|
|
763
|
+
[t.identifier('e')],
|
|
764
|
+
t.callExpression(
|
|
765
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
766
|
+
[t.memberExpression(
|
|
767
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
768
|
+
t.identifier('checked')
|
|
769
|
+
)]
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
]
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
);
|
|
763
776
|
}
|
|
764
777
|
continue;
|
|
765
778
|
}
|
|
@@ -769,28 +782,29 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
769
782
|
const expr = attr.value.expression;
|
|
770
783
|
const domName = normalizeAttrName(attrName);
|
|
771
784
|
|
|
772
|
-
if (isPotentiallyReactive(expr)) {
|
|
773
|
-
// Reactive attribute — wrap in effect
|
|
785
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
774
786
|
state.needsEffect = true;
|
|
775
|
-
|
|
776
|
-
t.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
787
|
+
const effectCall = t.callExpression(t.identifier('_$effect'), [
|
|
788
|
+
t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
|
|
789
|
+
]);
|
|
790
|
+
// In dev mode, add a leading comment when the effect wrapping is uncertain
|
|
791
|
+
// (non-signal function call whose args happen to contain signal reads)
|
|
792
|
+
if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
793
|
+
t.addComment(effectCall, 'leading',
|
|
794
|
+
' @what-dev: effect wrapping may be unnecessary — expression contains a non-signal function call with reactive args ',
|
|
795
|
+
false
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
statements.push(t.expressionStatement(effectCall));
|
|
782
799
|
} else {
|
|
783
800
|
// Static expression (no signal calls) — set once
|
|
784
801
|
statements.push(t.expressionStatement(buildSetPropCall(domName, expr)));
|
|
785
802
|
}
|
|
786
803
|
}
|
|
787
|
-
// Static string/boolean attributes already in template
|
|
788
804
|
}
|
|
789
805
|
}
|
|
790
806
|
|
|
791
807
|
function applyDynamicChildren(statements, elId, children, parentNode, state) {
|
|
792
|
-
// Build a child access path. We need to track position relative to template's children.
|
|
793
|
-
// Dynamic children (expressions and components) need insert() calls.
|
|
794
808
|
let childIndex = 0;
|
|
795
809
|
|
|
796
810
|
for (const child of children) {
|
|
@@ -807,16 +821,20 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
807
821
|
const marker = buildChildAccess(elId, childIndex);
|
|
808
822
|
state.needsInsert = true;
|
|
809
823
|
|
|
810
|
-
if (isPotentiallyReactive(expr)) {
|
|
811
|
-
|
|
812
|
-
t.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
824
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
825
|
+
const insertCall = t.callExpression(t.identifier('_$insert'), [
|
|
826
|
+
t.identifier(elId),
|
|
827
|
+
t.arrowFunctionExpression([], expr),
|
|
828
|
+
marker
|
|
829
|
+
]);
|
|
830
|
+
// In dev mode, add a leading comment when the reactive wrapping is uncertain
|
|
831
|
+
if (isUncertainReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
832
|
+
t.addComment(insertCall, 'leading',
|
|
833
|
+
' @what-dev: reactive wrapping may be unnecessary — expression contains a non-signal function call with reactive args ',
|
|
834
|
+
false
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
statements.push(t.expressionStatement(insertCall));
|
|
820
838
|
} else {
|
|
821
839
|
statements.push(
|
|
822
840
|
t.expressionStatement(
|
|
@@ -835,7 +853,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
835
853
|
if (t.isJSXElement(child)) {
|
|
836
854
|
const childTag = child.openingElement.name.name;
|
|
837
855
|
if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
|
|
838
|
-
// Component/control-flow — transform and insert
|
|
839
856
|
const transformed = transformElementFineGrained({ node: child }, state);
|
|
840
857
|
const marker = buildChildAccess(elId, childIndex);
|
|
841
858
|
state.needsInsert = true;
|
|
@@ -853,11 +870,10 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
853
870
|
// Static child element — already in template
|
|
854
871
|
// But check if it has dynamic children/attrs that need effects
|
|
855
872
|
const hasAnythingDynamic = child.openingElement.attributes.some(isDynamicAttr) ||
|
|
856
|
-
child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && a
|
|
873
|
+
child.openingElement.attributes.some(a => !t.isJSXSpreadAttribute(a) && getAttrName(a)?.startsWith('on')) ||
|
|
857
874
|
!child.children.every(isStaticChild);
|
|
858
875
|
|
|
859
876
|
if (hasAnythingDynamic) {
|
|
860
|
-
// Need to reference this child element and apply effects to it
|
|
861
877
|
const childElId = state.nextVarId();
|
|
862
878
|
statements.push(
|
|
863
879
|
t.variableDeclaration('const', [
|
|
@@ -876,12 +892,11 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
876
892
|
}
|
|
877
893
|
|
|
878
894
|
if (t.isJSXFragment(child)) {
|
|
879
|
-
// Inline fragment children
|
|
880
895
|
for (const fChild of child.children) {
|
|
881
896
|
if (t.isJSXExpressionContainer(fChild) && !t.isJSXEmptyExpression(fChild.expression)) {
|
|
882
897
|
state.needsInsert = true;
|
|
883
898
|
const expr = fChild.expression;
|
|
884
|
-
if (isPotentiallyReactive(expr)) {
|
|
899
|
+
if (isPotentiallyReactive(expr, state.signalNames, state.importedIdentifiers)) {
|
|
885
900
|
statements.push(
|
|
886
901
|
t.expressionStatement(
|
|
887
902
|
t.callExpression(t.identifier('_$insert'), [
|
|
@@ -907,19 +922,202 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
907
922
|
}
|
|
908
923
|
|
|
909
924
|
function buildChildAccess(elId, index) {
|
|
910
|
-
// Use
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
t.memberExpression(t.identifier(elId), t.identifier('
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
);
|
|
925
|
+
// Use firstChild/nextSibling chains instead of childNodes[N]
|
|
926
|
+
// This is more robust with whitespace text nodes
|
|
927
|
+
if (index === 0) {
|
|
928
|
+
return t.memberExpression(t.identifier(elId), t.identifier('firstChild'));
|
|
929
|
+
}
|
|
930
|
+
// Chain .nextSibling for subsequent indices
|
|
931
|
+
let expr = t.memberExpression(t.identifier(elId), t.identifier('firstChild'));
|
|
932
|
+
for (let i = 0; i < index; i++) {
|
|
933
|
+
expr = t.memberExpression(expr, t.identifier('nextSibling'));
|
|
934
|
+
}
|
|
935
|
+
return expr;
|
|
917
936
|
}
|
|
918
937
|
|
|
919
938
|
function transformComponentFineGrained(path, state) {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
939
|
+
const { node } = path;
|
|
940
|
+
const openingElement = node.openingElement;
|
|
941
|
+
const componentName = openingElement.name.name;
|
|
942
|
+
const attributes = openingElement.attributes;
|
|
943
|
+
const children = node.children;
|
|
944
|
+
|
|
945
|
+
// Check for client: directive (islands)
|
|
946
|
+
let clientDirective = null;
|
|
947
|
+
const filteredAttrs = [];
|
|
948
|
+
|
|
949
|
+
for (const attr of attributes) {
|
|
950
|
+
if (t.isJSXAttribute(attr)) {
|
|
951
|
+
// Handle both simple names and namespaced names (client:idle)
|
|
952
|
+
let name;
|
|
953
|
+
if (t.isJSXNamespacedName(attr.name)) {
|
|
954
|
+
name = `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
955
|
+
} else {
|
|
956
|
+
name = attr.name.name;
|
|
957
|
+
}
|
|
958
|
+
if (name && typeof name === 'string' && name.startsWith('client:')) {
|
|
959
|
+
const mode = name.slice(7);
|
|
960
|
+
if (attr.value) {
|
|
961
|
+
clientDirective = { type: mode, value: attr.value.value };
|
|
962
|
+
} else {
|
|
963
|
+
clientDirective = { type: mode };
|
|
964
|
+
}
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
filteredAttrs.push(attr);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (clientDirective) {
|
|
972
|
+
state.needsCreateComponent = true;
|
|
973
|
+
state.needsIsland = true;
|
|
974
|
+
|
|
975
|
+
const islandProps = [
|
|
976
|
+
t.objectProperty(t.identifier('component'), t.identifier(componentName)),
|
|
977
|
+
t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
|
|
978
|
+
];
|
|
979
|
+
|
|
980
|
+
if (clientDirective.value) {
|
|
981
|
+
islandProps.push(
|
|
982
|
+
t.objectProperty(t.identifier('mediaQuery'), t.stringLiteral(clientDirective.value))
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
for (const attr of filteredAttrs) {
|
|
987
|
+
if (t.isJSXSpreadAttribute(attr)) continue;
|
|
988
|
+
const attrName = getAttrName(attr);
|
|
989
|
+
const value = getAttributeValue(attr.value);
|
|
990
|
+
islandProps.push(t.objectProperty(t.identifier(attrName), value));
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return t.callExpression(
|
|
994
|
+
t.identifier('_$createComponent'),
|
|
995
|
+
[t.identifier('Island'), t.objectExpression(islandProps), t.arrayExpression([])]
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Regular component — use _$createComponent to instantiate, component runs once
|
|
1000
|
+
state.needsCreateComponent = true;
|
|
1001
|
+
|
|
1002
|
+
const props = [];
|
|
1003
|
+
let hasSpread = false;
|
|
1004
|
+
let spreadExpr = null;
|
|
1005
|
+
|
|
1006
|
+
for (const attr of filteredAttrs) {
|
|
1007
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
1008
|
+
hasSpread = true;
|
|
1009
|
+
spreadExpr = attr.argument;
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const attrName = getAttrName(attr);
|
|
1014
|
+
|
|
1015
|
+
// Handle bind: attributes for components
|
|
1016
|
+
if (isBindingAttribute(attrName)) {
|
|
1017
|
+
const bindProp = getBindingProperty(attrName);
|
|
1018
|
+
const signalExpr = attr.value.expression;
|
|
1019
|
+
|
|
1020
|
+
if (bindProp === 'value') {
|
|
1021
|
+
props.push(
|
|
1022
|
+
t.objectProperty(t.identifier('value'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
1023
|
+
);
|
|
1024
|
+
props.push(
|
|
1025
|
+
t.objectProperty(
|
|
1026
|
+
t.identifier('onInput'),
|
|
1027
|
+
t.arrowFunctionExpression(
|
|
1028
|
+
[t.identifier('e')],
|
|
1029
|
+
t.callExpression(
|
|
1030
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
1031
|
+
[t.memberExpression(
|
|
1032
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
1033
|
+
t.identifier('value')
|
|
1034
|
+
)]
|
|
1035
|
+
)
|
|
1036
|
+
)
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
} else if (bindProp === 'checked') {
|
|
1040
|
+
props.push(
|
|
1041
|
+
t.objectProperty(t.identifier('checked'), t.callExpression(t.cloneNode(signalExpr), []))
|
|
1042
|
+
);
|
|
1043
|
+
props.push(
|
|
1044
|
+
t.objectProperty(
|
|
1045
|
+
t.identifier('onChange'),
|
|
1046
|
+
t.arrowFunctionExpression(
|
|
1047
|
+
[t.identifier('e')],
|
|
1048
|
+
t.callExpression(
|
|
1049
|
+
t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
|
|
1050
|
+
[t.memberExpression(
|
|
1051
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
1052
|
+
t.identifier('checked')
|
|
1053
|
+
)]
|
|
1054
|
+
)
|
|
1055
|
+
)
|
|
1056
|
+
)
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Handle event modifiers on components
|
|
1063
|
+
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
1064
|
+
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
1065
|
+
const handler = getAttributeValue(attr.value);
|
|
1066
|
+
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
1067
|
+
props.push(t.objectProperty(t.identifier(eventName), wrappedHandler));
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const value = getAttributeValue(attr.value);
|
|
1072
|
+
|
|
1073
|
+
props.push(
|
|
1074
|
+
t.objectProperty(
|
|
1075
|
+
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(attrName)
|
|
1076
|
+
? t.identifier(attrName)
|
|
1077
|
+
: t.stringLiteral(attrName),
|
|
1078
|
+
value
|
|
1079
|
+
)
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Transform children
|
|
1084
|
+
const transformedChildren = [];
|
|
1085
|
+
for (const child of children) {
|
|
1086
|
+
if (t.isJSXText(child)) {
|
|
1087
|
+
const text = child.value.replace(/\n\s+/g, ' ').trim();
|
|
1088
|
+
if (text) transformedChildren.push(t.stringLiteral(text));
|
|
1089
|
+
} else if (t.isJSXExpressionContainer(child)) {
|
|
1090
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1091
|
+
transformedChildren.push(child.expression);
|
|
1092
|
+
}
|
|
1093
|
+
} else if (t.isJSXElement(child)) {
|
|
1094
|
+
transformedChildren.push(transformElementFineGrained({ node: child }, state));
|
|
1095
|
+
} else if (t.isJSXFragment(child)) {
|
|
1096
|
+
transformedChildren.push(transformFragmentFineGrained({ node: child }, state));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
let propsExpr;
|
|
1101
|
+
if (hasSpread) {
|
|
1102
|
+
if (props.length > 0) {
|
|
1103
|
+
propsExpr = t.callExpression(
|
|
1104
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
1105
|
+
[t.objectExpression([]), spreadExpr, t.objectExpression(props)]
|
|
1106
|
+
);
|
|
1107
|
+
} else {
|
|
1108
|
+
propsExpr = spreadExpr;
|
|
1109
|
+
}
|
|
1110
|
+
} else if (props.length > 0) {
|
|
1111
|
+
propsExpr = t.objectExpression(props);
|
|
1112
|
+
} else {
|
|
1113
|
+
propsExpr = t.nullLiteral();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const childrenArray = transformedChildren.length > 0
|
|
1117
|
+
? t.arrayExpression(transformedChildren)
|
|
1118
|
+
: t.arrayExpression([]);
|
|
1119
|
+
|
|
1120
|
+
return t.callExpression(t.identifier('_$createComponent'), [t.identifier(componentName), propsExpr, childrenArray]);
|
|
923
1121
|
}
|
|
924
1122
|
|
|
925
1123
|
function transformForFineGrained(path, state) {
|
|
@@ -931,17 +1129,17 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
931
1129
|
// → mapArray(data, (item) => ...)
|
|
932
1130
|
let eachExpr = null;
|
|
933
1131
|
for (const attr of attributes) {
|
|
934
|
-
if (t.isJSXAttribute(attr) && attr
|
|
1132
|
+
if (t.isJSXAttribute(attr) && getAttrName(attr) === 'each') {
|
|
935
1133
|
eachExpr = getAttributeValue(attr.value);
|
|
936
1134
|
}
|
|
937
1135
|
}
|
|
938
1136
|
|
|
939
1137
|
if (!eachExpr) {
|
|
940
|
-
|
|
941
|
-
|
|
1138
|
+
console.warn('[what-compiler] <For> element missing "each" attribute.');
|
|
1139
|
+
state.needsH = true;
|
|
1140
|
+
return transformElementAsH(path, state);
|
|
942
1141
|
}
|
|
943
1142
|
|
|
944
|
-
// Get the render function from children
|
|
945
1143
|
let renderFn = null;
|
|
946
1144
|
for (const child of children) {
|
|
947
1145
|
if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
@@ -951,7 +1149,9 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
951
1149
|
}
|
|
952
1150
|
|
|
953
1151
|
if (!renderFn) {
|
|
954
|
-
|
|
1152
|
+
console.warn('[what-compiler] <For> element missing render function child.');
|
|
1153
|
+
state.needsH = true;
|
|
1154
|
+
return transformElementAsH(path, state);
|
|
955
1155
|
}
|
|
956
1156
|
|
|
957
1157
|
state.needsMapArray = true;
|
|
@@ -959,20 +1159,16 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
959
1159
|
}
|
|
960
1160
|
|
|
961
1161
|
function transformShowFineGrained(path, state) {
|
|
962
|
-
const { node } = path;
|
|
963
|
-
const attributes = node.openingElement.attributes;
|
|
964
|
-
const children = node.children;
|
|
965
|
-
|
|
966
1162
|
// <Show when={cond}>{content}</Show>
|
|
967
|
-
//
|
|
968
|
-
|
|
1163
|
+
// Uses _$createComponent(Show, ...) — Show is a runtime component
|
|
1164
|
+
state.needsCreateComponent = true;
|
|
1165
|
+
return transformComponentFineGrained(path, state);
|
|
969
1166
|
}
|
|
970
1167
|
|
|
971
1168
|
function transformFragmentFineGrained(path, state) {
|
|
972
1169
|
const { node } = path;
|
|
973
1170
|
const children = node.children;
|
|
974
1171
|
|
|
975
|
-
// Fragments with fine-grained: just return children array or single child
|
|
976
1172
|
const transformed = [];
|
|
977
1173
|
for (const child of children) {
|
|
978
1174
|
if (t.isJSXText(child)) {
|
|
@@ -993,8 +1189,15 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
993
1189
|
return t.arrayExpression(transformed);
|
|
994
1190
|
}
|
|
995
1191
|
|
|
996
|
-
|
|
997
|
-
|
|
1192
|
+
// Template deduplication: same HTML string → same module-level const
|
|
1193
|
+
function getOrCreateTemplate(state, html) {
|
|
1194
|
+
if (state.templateMap.has(html)) {
|
|
1195
|
+
return state.templateMap.get(html);
|
|
1196
|
+
}
|
|
1197
|
+
const id = `_tmpl$${state.templateCount++}`;
|
|
1198
|
+
state.templateMap.set(html, id);
|
|
1199
|
+
state.templates.push({ id, html });
|
|
1200
|
+
return id;
|
|
998
1201
|
}
|
|
999
1202
|
|
|
1000
1203
|
// =====================================================
|
|
@@ -1007,15 +1210,6 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1007
1210
|
visitor: {
|
|
1008
1211
|
Program: {
|
|
1009
1212
|
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
1213
|
// Fine-grained mode state
|
|
1020
1214
|
state.needsTemplate = false;
|
|
1021
1215
|
state.needsInsert = false;
|
|
@@ -1023,147 +1217,252 @@ export default function whatBabelPlugin({ types: t }) {
|
|
|
1023
1217
|
state.needsMapArray = false;
|
|
1024
1218
|
state.needsSpread = false;
|
|
1025
1219
|
state.needsSetProp = false;
|
|
1220
|
+
state.needsH = false;
|
|
1221
|
+
state.needsCreateComponent = false;
|
|
1222
|
+
state.needsFragment = false;
|
|
1223
|
+
state.needsIsland = false;
|
|
1224
|
+
state.needsDelegation = false;
|
|
1225
|
+
state.delegatedEvents = new Set();
|
|
1026
1226
|
state.templates = [];
|
|
1227
|
+
state.templateMap = new Map(); // html → template id (deduplication)
|
|
1027
1228
|
state.templateCount = 0;
|
|
1028
1229
|
state._varCounter = 0;
|
|
1230
|
+
state._pendingSetup = [];
|
|
1029
1231
|
state.nextVarId = () => `_el$${state._varCounter++}`;
|
|
1030
|
-
},
|
|
1031
1232
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
);
|
|
1062
|
-
}
|
|
1063
|
-
if (state.needsMapArray) {
|
|
1064
|
-
fgSpecifiers.push(
|
|
1065
|
-
t.importSpecifier(t.identifier('_$mapArray'), t.identifier('mapArray'))
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
if (state.needsSpread) {
|
|
1069
|
-
fgSpecifiers.push(
|
|
1070
|
-
t.importSpecifier(t.identifier('_$spread'), t.identifier('spread'))
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
if (state.needsSetProp) {
|
|
1074
|
-
fgSpecifiers.push(
|
|
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
|
-
}
|
|
1233
|
+
// Collect signal names for smart reactivity detection
|
|
1234
|
+
state.signalNames = new Set();
|
|
1235
|
+
|
|
1236
|
+
// --- Imported Signal Tracking ---
|
|
1237
|
+
// Only mark imports as potentially reactive if they come from known
|
|
1238
|
+
// reactive sources: what-framework, what-framework/*, relative paths
|
|
1239
|
+
// (user stores), or functions matching use*/create* naming conventions.
|
|
1240
|
+
// This prevents over-wrapping of utility imports (lodash, etc.).
|
|
1241
|
+
state.importedIdentifiers = new Set();
|
|
1242
|
+
for (const node of path.node.body) {
|
|
1243
|
+
if (t.isImportDeclaration(node)) {
|
|
1244
|
+
const source = node.source.value;
|
|
1245
|
+
const isReactiveSource =
|
|
1246
|
+
source === 'what-framework' ||
|
|
1247
|
+
source.startsWith('what-framework/') ||
|
|
1248
|
+
source === 'what-core' ||
|
|
1249
|
+
source.startsWith('what-core/') ||
|
|
1250
|
+
source.startsWith('./') ||
|
|
1251
|
+
source.startsWith('../');
|
|
1252
|
+
|
|
1253
|
+
for (const spec of node.specifiers) {
|
|
1254
|
+
let localName = null;
|
|
1255
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1256
|
+
localName = spec.local.name;
|
|
1257
|
+
} else if (t.isImportDefaultSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1258
|
+
localName = spec.local.name;
|
|
1259
|
+
} else if (t.isImportNamespaceSpecifier(spec) && t.isIdentifier(spec.local)) {
|
|
1260
|
+
localName = spec.local.name;
|
|
1261
|
+
}
|
|
1096
1262
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
node.source.value === 'what-core/render'
|
|
1104
|
-
)) {
|
|
1105
|
-
existingRenderImport = node;
|
|
1106
|
-
break;
|
|
1263
|
+
if (localName) {
|
|
1264
|
+
// Mark as reactive if from a reactive source, or if the name
|
|
1265
|
+
// matches use*/create* conventions (hooks/signal creators)
|
|
1266
|
+
if (isReactiveSource || /^(use|create)[A-Z]/.test(localName)) {
|
|
1267
|
+
state.importedIdentifiers.add(localName);
|
|
1268
|
+
}
|
|
1107
1269
|
}
|
|
1108
1270
|
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1109
1273
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1274
|
+
path.traverse({
|
|
1275
|
+
VariableDeclarator(declPath) {
|
|
1276
|
+
const init = declPath.node.init;
|
|
1277
|
+
if (!init || !t.isCallExpression(init)) return;
|
|
1278
|
+
|
|
1279
|
+
const callee = init.callee;
|
|
1280
|
+
let calleeName = '';
|
|
1281
|
+
if (t.isIdentifier(callee)) {
|
|
1282
|
+
calleeName = callee.name;
|
|
1283
|
+
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
|
|
1284
|
+
calleeName = callee.property.name;
|
|
1285
|
+
}
|
|
1116
1286
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1287
|
+
if (SIGNAL_CREATORS.has(calleeName)) {
|
|
1288
|
+
const id = declPath.node.id;
|
|
1289
|
+
if (t.isIdentifier(id)) {
|
|
1290
|
+
state.signalNames.add(id.name);
|
|
1291
|
+
} else if (t.isArrayPattern(id)) {
|
|
1292
|
+
for (const el of id.elements) {
|
|
1293
|
+
if (t.isIdentifier(el)) state.signalNames.add(el.name);
|
|
1294
|
+
}
|
|
1295
|
+
} else if (t.isObjectPattern(id)) {
|
|
1296
|
+
for (const prop of id.properties) {
|
|
1297
|
+
if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
|
|
1298
|
+
state.signalNames.add(prop.value.name);
|
|
1299
|
+
}
|
|
1120
1300
|
}
|
|
1121
1301
|
}
|
|
1122
|
-
} else {
|
|
1123
|
-
path.unshiftContainer('body',
|
|
1124
|
-
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1125
|
-
);
|
|
1126
1302
|
}
|
|
1127
1303
|
}
|
|
1304
|
+
});
|
|
1305
|
+
},
|
|
1306
|
+
|
|
1307
|
+
exit(path, state) {
|
|
1308
|
+
// Insert template declarations at top of program (hoisted to module scope)
|
|
1309
|
+
for (const tmpl of state.templates.reverse()) {
|
|
1310
|
+
path.unshiftContainer('body',
|
|
1311
|
+
t.variableDeclaration('const', [
|
|
1312
|
+
t.variableDeclarator(
|
|
1313
|
+
t.identifier(tmpl.id),
|
|
1314
|
+
t.callExpression(t.identifier('_$template'), [t.stringLiteral(tmpl.html)])
|
|
1315
|
+
)
|
|
1316
|
+
])
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1128
1319
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1320
|
+
// Build fine-grained imports
|
|
1321
|
+
const fgSpecifiers = [];
|
|
1322
|
+
if (state.needsTemplate) {
|
|
1323
|
+
fgSpecifiers.push(
|
|
1324
|
+
t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
if (state.needsInsert) {
|
|
1328
|
+
fgSpecifiers.push(
|
|
1329
|
+
t.importSpecifier(t.identifier('_$insert'), t.identifier('insert'))
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (state.needsEffect) {
|
|
1333
|
+
fgSpecifiers.push(
|
|
1334
|
+
t.importSpecifier(t.identifier('_$effect'), t.identifier('effect'))
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
if (state.needsMapArray) {
|
|
1338
|
+
fgSpecifiers.push(
|
|
1339
|
+
t.importSpecifier(t.identifier('_$mapArray'), t.identifier('mapArray'))
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
if (state.needsSpread) {
|
|
1343
|
+
fgSpecifiers.push(
|
|
1344
|
+
t.importSpecifier(t.identifier('_$spread'), t.identifier('spread'))
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
if (state.needsSetProp) {
|
|
1348
|
+
fgSpecifiers.push(
|
|
1349
|
+
t.importSpecifier(t.identifier('_$setProp'), t.identifier('setProp'))
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
if (state.needsCreateComponent) {
|
|
1353
|
+
fgSpecifiers.push(
|
|
1354
|
+
t.importSpecifier(t.identifier('_$createComponent'), t.identifier('_$createComponent'))
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (state.needsDelegation) {
|
|
1358
|
+
fgSpecifiers.push(
|
|
1359
|
+
t.importSpecifier(t.identifier('_$delegateEvents'), t.identifier('delegateEvents'))
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Core imports (h/Fragment/Island for components)
|
|
1364
|
+
const coreSpecifiers = [];
|
|
1365
|
+
if (state.needsH) {
|
|
1366
|
+
coreSpecifiers.push(
|
|
1367
|
+
t.importSpecifier(t.identifier('h'), t.identifier('h'))
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
if (state.needsFragment) {
|
|
1371
|
+
coreSpecifiers.push(
|
|
1372
|
+
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment'))
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
if (state.needsIsland) {
|
|
1376
|
+
coreSpecifiers.push(
|
|
1377
|
+
t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (fgSpecifiers.length > 0) {
|
|
1382
|
+
let existingRenderImport = null;
|
|
1383
|
+
for (const node of path.node.body) {
|
|
1384
|
+
if (t.isImportDeclaration(node) && (
|
|
1385
|
+
node.source.value === 'what-framework/render' ||
|
|
1386
|
+
node.source.value === 'what-core/render'
|
|
1387
|
+
)) {
|
|
1388
|
+
existingRenderImport = node;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1131
1391
|
}
|
|
1132
1392
|
|
|
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'))
|
|
1393
|
+
if (existingRenderImport) {
|
|
1394
|
+
const existingNames = new Set(
|
|
1395
|
+
existingRenderImport.specifiers
|
|
1396
|
+
.filter(s => t.isImportSpecifier(s))
|
|
1397
|
+
.map(s => s.imported.name)
|
|
1143
1398
|
);
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1399
|
+
for (const spec of fgSpecifiers) {
|
|
1400
|
+
if (!existingNames.has(spec.imported.name)) {
|
|
1401
|
+
existingRenderImport.specifiers.push(spec);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
path.unshiftContainer('body',
|
|
1406
|
+
t.importDeclaration(fgSpecifiers, t.stringLiteral('what-framework/render'))
|
|
1148
1407
|
);
|
|
1149
1408
|
}
|
|
1409
|
+
}
|
|
1150
1410
|
|
|
1411
|
+
if (coreSpecifiers.length > 0) {
|
|
1151
1412
|
addCoreImports(path, t, coreSpecifiers);
|
|
1152
1413
|
}
|
|
1414
|
+
|
|
1415
|
+
// Emit event delegation setup call if any delegated events were used
|
|
1416
|
+
if (state.needsDelegation && state.delegatedEvents && state.delegatedEvents.size > 0) {
|
|
1417
|
+
const eventArray = t.arrayExpression(
|
|
1418
|
+
[...state.delegatedEvents].map(e => t.stringLiteral(e))
|
|
1419
|
+
);
|
|
1420
|
+
path.pushContainer('body',
|
|
1421
|
+
t.expressionStatement(
|
|
1422
|
+
t.callExpression(t.identifier('_$delegateEvents'), [eventArray])
|
|
1423
|
+
)
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1153
1426
|
}
|
|
1154
1427
|
},
|
|
1155
1428
|
|
|
1156
1429
|
JSXElement(path, state) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
path
|
|
1430
|
+
// FIX-1: Use scope-aware signal detection instead of file-global
|
|
1431
|
+
state.signalNames = collectSignalNamesFromScope(path);
|
|
1432
|
+
state._pendingSetup = [];
|
|
1433
|
+
const transformed = transformElementFineGrained(path, state);
|
|
1434
|
+
const pending = state._pendingSetup;
|
|
1435
|
+
state._pendingSetup = [];
|
|
1436
|
+
|
|
1437
|
+
if (pending.length > 0) {
|
|
1438
|
+
// Find the enclosing statement to hoist setup before it
|
|
1439
|
+
let stmtPath = path;
|
|
1440
|
+
while (stmtPath && !stmtPath.isStatement()) {
|
|
1441
|
+
stmtPath = stmtPath.parentPath;
|
|
1442
|
+
}
|
|
1443
|
+
if (stmtPath && stmtPath.isStatement()) {
|
|
1444
|
+
// Insert setup statements before the enclosing statement
|
|
1445
|
+
for (const stmt of pending) {
|
|
1446
|
+
stmtPath.insertBefore(stmt);
|
|
1447
|
+
}
|
|
1448
|
+
path.replaceWith(transformed);
|
|
1449
|
+
} else {
|
|
1450
|
+
// Fallback: if we can't find a statement parent, use IIFE
|
|
1451
|
+
pending.push(t.returnStatement(transformed));
|
|
1452
|
+
path.replaceWith(
|
|
1453
|
+
t.callExpression(
|
|
1454
|
+
t.arrowFunctionExpression([], t.blockStatement(pending)),
|
|
1455
|
+
[]
|
|
1456
|
+
)
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
} else {
|
|
1460
|
+
path.replaceWith(transformed);
|
|
1461
|
+
}
|
|
1161
1462
|
},
|
|
1162
1463
|
|
|
1163
1464
|
JSXFragment(path, state) {
|
|
1164
|
-
const transformed = state
|
|
1165
|
-
? transformFragmentFineGrained(path, state)
|
|
1166
|
-
: transformFragmentVdom(path, state);
|
|
1465
|
+
const transformed = transformFragmentFineGrained(path, state);
|
|
1167
1466
|
path.replaceWith(transformed);
|
|
1168
1467
|
}
|
|
1169
1468
|
}
|
|
@@ -1187,7 +1486,6 @@ function addCoreImports(path, t, coreSpecifiers) {
|
|
|
1187
1486
|
.filter(s => t.isImportSpecifier(s))
|
|
1188
1487
|
.map(s => s.imported.name)
|
|
1189
1488
|
);
|
|
1190
|
-
|
|
1191
1489
|
for (const spec of coreSpecifiers) {
|
|
1192
1490
|
if (!existingNames.has(spec.imported.name)) {
|
|
1193
1491
|
existingImport.specifiers.push(spec);
|