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.
@@ -1,47 +1,55 @@
1
1
  /**
2
- * What Framework Babel Plugin
2
+ * What Framework Babel Plugin — Fine-Grained Only
3
3
  *
4
- * Two modes:
5
- * - 'vdom' (legacy): JSX h() calls through VNode reconciler
6
- * - 'fine-grained' (default): JSX template() + insert() + effect() calls
7
- * Static HTML extracted to templates, dynamic expressions wrapped in effects.
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
- * Fine-grained output:
10
- * const _t$ = template('<div class="container"><h1>Title</h1><p></p></div>');
8
+ * Output:
9
+ * const _tmpl$1 = template('<div class="container"><h1>Title</h1><p></p></div>');
11
10
  * function App() {
12
- * const _el$ = _t$();
13
- * insert(_el$.children[1], () => desc());
11
+ * const _el$ = _tmpl$1();
12
+ * insert(_el$.childNodes[1], () => desc());
14
13
  * return _el$;
15
14
  * }
16
15
  *
17
- * VDOM output (legacy):
18
- * h('div', { class: 'container' }, h('h1', null, 'Title'), h('p', null, desc()))
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
- 'base',
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
- export default function whatBabelPlugin({ types: t }) {
41
- const mode = 'fine-grained'; // Can be overridden via plugin options
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 (used by both modes)
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
- // VDOM Mode (legacy h() calls)
169
+ // Reactivity Detection Signal-Aware
154
170
  // =====================================================
155
171
 
156
- function transformChildrenVdom(children, state) {
157
- const result = [];
158
- for (const child of children) {
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
- function transformElementVdom(path, state) {
176
- const { node } = path;
177
- const openingElement = node.openingElement;
178
- const tagName = openingElement.name.name;
179
- const attributes = openingElement.attributes;
180
- const children = node.children;
181
-
182
- if (isComponent(tagName)) {
183
- return transformComponentVdom(path, state);
184
- }
185
-
186
- const props = [];
187
- let hasSpread = false;
188
- let spreadExpr = null;
189
-
190
- for (const attr of attributes) {
191
- if (t.isJSXSpreadAttribute(attr)) {
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 (isBindingAttribute(attrName)) {
242
- const bindProp = getBindingProperty(attrName);
243
- const signalExpr = attr.value.expression;
244
-
245
- if (bindProp === 'value') {
246
- props.push(
247
- t.objectProperty(t.identifier('value'), t.callExpression(t.cloneNode(signalExpr), []))
248
- );
249
- props.push(
250
- t.objectProperty(
251
- t.identifier('onInput'),
252
- t.arrowFunctionExpression(
253
- [t.identifier('e')],
254
- t.callExpression(
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
- const transformedChildren = transformChildrenVdom(children, state);
303
-
304
- let propsExpr;
305
- if (hasSpread) {
306
- if (props.length > 0) {
307
- propsExpr = t.callExpression(
308
- t.memberExpression(t.identifier('Object'), t.identifier('assign')),
309
- [t.objectExpression([]), spreadExpr, t.objectExpression(props)]
310
- );
311
- } else {
312
- propsExpr = spreadExpr;
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
- } else if (props.length > 0) {
315
- propsExpr = t.objectExpression(props);
316
- } else {
317
- propsExpr = t.nullLiteral();
240
+ scope = scope.parent;
318
241
  }
319
242
 
320
- const args = [t.stringLiteral(tagName), propsExpr, ...transformedChildren];
321
- state.needsH = true;
322
- return t.callExpression(t.identifier('h'), args);
243
+ return signalNames;
323
244
  }
324
245
 
325
- function transformComponentVdom(path, state) {
326
- const { node } = path;
327
- const openingElement = node.openingElement;
328
- const componentName = openingElement.name.name;
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
- let clientDirective = null;
333
- const filteredAttrs = [];
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
- for (const attr of attributes) {
336
- if (t.isJSXAttribute(attr)) {
337
- const name = attr.name.name;
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
- if (clientDirective) {
352
- state.needsH = true;
353
- state.needsIsland = true;
261
+ // parseInt(), isNaN(), etc.
262
+ if (t.isIdentifier(callee)) {
263
+ return SAFE_GLOBAL_CALLS.has(callee.name);
264
+ }
354
265
 
355
- const islandProps = [
356
- t.objectProperty(t.identifier('component'), t.identifier(componentName)),
357
- t.objectProperty(t.identifier('mode'), t.stringLiteral(clientDirective.type)),
358
- ];
266
+ return false;
267
+ }
359
268
 
360
- if (clientDirective.value) {
361
- islandProps.push(
362
- t.objectProperty(t.identifier('mediaQuery'), t.stringLiteral(clientDirective.value))
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
- for (const attr of filteredAttrs) {
367
- if (t.isJSXSpreadAttribute(attr)) continue;
368
- const attrName = attr.name.name;
369
- const value = getAttributeValue(attr.value);
370
- islandProps.push(t.objectProperty(t.identifier(attrName), value));
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
- return t.callExpression(
374
- t.identifier('h'),
375
- [t.identifier('Island'), t.objectExpression(islandProps)]
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
- const props = [];
380
- let hasSpread = false;
381
- let spreadExpr = null;
341
+ if (t.isIdentifier(expr)) {
342
+ return isSignalIdentifier(expr.name, signalNames);
343
+ }
382
344
 
383
- for (const attr of filteredAttrs) {
384
- if (t.isJSXSpreadAttribute(attr)) {
385
- hasSpread = true;
386
- spreadExpr = attr.argument;
387
- continue;
388
- }
345
+ if (t.isMemberExpression(expr)) {
346
+ return isPotentiallyReactive(expr.object, signalNames, importedIds);
347
+ }
389
348
 
390
- const attrName = attr.name.name;
391
- const value = getAttributeValue(attr.value);
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
- props.push(
394
- t.objectProperty(
395
- /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(attrName)
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
- const transformedChildren = transformChildrenVdom(children, state);
360
+ if (t.isUnaryExpression(expr)) {
361
+ return isPotentiallyReactive(expr.argument, signalNames, importedIds);
362
+ }
404
363
 
405
- let propsExpr;
406
- if (hasSpread) {
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
- const args = [t.identifier(componentName), propsExpr, ...transformedChildren];
422
- state.needsH = true;
423
- return t.callExpression(t.identifier('h'), args);
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
- function transformFragmentVdom(path, state) {
427
- const { node } = path;
428
- const transformedChildren = transformChildrenVdom(node.children, state);
374
+ if (t.isArrayExpression(expr)) {
375
+ return expr.elements.some(el => el && isPotentiallyReactive(el, signalNames, importedIds));
376
+ }
429
377
 
430
- state.needsH = true;
431
- state.needsFragment = true;
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 t.callExpression(
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 (expression, not string literal)
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; // boolean attr like `disabled`
411
+ if (!attr.value) return false;
469
412
  return t.isJSXExpressionContainer(attr.value);
470
413
  }
471
414
 
472
- // Check if an expression is potentially reactive (contains function calls)
473
- function isPotentiallyReactive(expr) {
474
- if (t.isCallExpression(expr)) return true;
475
- if (t.isMemberExpression(expr)) return isPotentiallyReactive(expr.object);
476
- if (t.isConditionalExpression(expr)) {
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
- if (t.isBinaryExpression(expr) || t.isLogicalExpression(expr)) {
480
- return isPotentiallyReactive(expr.left) || isPotentiallyReactive(expr.right);
481
- }
482
- if (t.isTemplateLiteral(expr)) {
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.name.name;
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 — skip from template, will be set via effect
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, '&amp;').replace(/"/g, '&quot;');
493
+ return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
- // For <For> and <Show> control flow components
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.name?.name;
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 = generateTemplateId(state);
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
- // Fallback to VDOM mode for degenerate cases
626
- return transformElementVdom(path, state);
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 = generateTemplateId(state);
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$ = _t$()
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
- // Return the element wrap in IIFE
649
- statements.push(t.returnStatement(t.identifier(elId)));
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
- return t.callExpression(
652
- t.arrowFunctionExpression([], t.blockStatement(statements)),
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.name.name;
643
+ const attrName = getAttrName(attr);
680
644
 
681
- // Event handlers
682
- if (attrName.startsWith('on') && !attrName.includes('|')) {
683
- const event = attrName.slice(2).toLowerCase();
684
- const handler = getAttributeValue(attr.value);
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.callExpression(
689
- t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
690
- [t.stringLiteral(event), handler]
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
- statements.push(
776
- t.expressionStatement(
777
- t.callExpression(t.identifier('_$effect'), [
778
- t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
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
- // Build a child access path. We need to track position relative to template's children.
793
- // Dynamic children (expressions and components) need insert() calls.
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
- const expr = child.expression;
807
- const marker = buildChildAccess(elId, childIndex);
808
- state.needsInsert = true;
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
- if (isPotentiallyReactive(expr)) {
811
- statements.push(
812
- t.expressionStatement(
813
- t.callExpression(t.identifier('_$insert'), [
814
- t.identifier(elId),
815
- t.arrowFunctionExpression([], expr),
816
- marker
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 (t.isJSXElement(child)) {
836
- const childTag = child.openingElement.name.name;
837
- if (isComponent(childTag) || childTag === 'For' || childTag === 'Show') {
838
- // Component/control-flow — transform and insert
839
- const transformed = transformElementFineGrained({ node: child }, state);
840
- const marker = buildChildAccess(elId, childIndex);
841
- state.needsInsert = true;
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.expressionStatement(
844
- t.callExpression(t.identifier('_$insert'), [
845
- t.identifier(elId),
846
- transformed,
847
- marker
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 (t.isJSXFragment(child)) {
879
- // Inline fragment children
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 childNodes[n] (not children[n]) so indices remain stable when text/comment
911
- // placeholders are present in the static template.
912
- return t.memberExpression(
913
- t.memberExpression(t.identifier(elId), t.identifier('childNodes')),
914
- t.numericLiteral(index),
915
- true // computed
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
- // Components in fine-grained mode still use h() for now (backward compat)
921
- // The component itself decides how to render (vdom or fine-grained)
922
- return transformComponentVdom(path, state);
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.name.name === 'each') {
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
- // Fallback
941
- return transformElementVdom(path, state);
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
- return transformElementVdom(path, state);
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
- // Still uses h(Show, ...) for now — Show is a runtime component
968
- return transformElementVdom(path, state);
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
- function generateTemplateId(state) {
997
- return `_t$${state.templateCount++}`;
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
- // Build imports
1047
- const fgSpecifiers = [];
1048
- if (state.needsTemplate) {
1049
- fgSpecifiers.push(
1050
- t.importSpecifier(t.identifier('_$template'), t.identifier('template'))
1051
- );
1052
- }
1053
- if (state.needsInsert) {
1054
- fgSpecifiers.push(
1055
- t.importSpecifier(t.identifier('_$insert'), t.identifier('insert'))
1056
- );
1057
- }
1058
- if (state.needsEffect) {
1059
- fgSpecifiers.push(
1060
- t.importSpecifier(t.identifier('_$effect'), t.identifier('effect'))
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
- }
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
- if (fgSpecifiers.length > 0) {
1098
- // Check for existing render import
1099
- let existingRenderImport = null;
1100
- for (const node of path.node.body) {
1101
- if (t.isImportDeclaration(node) && (
1102
- node.source.value === 'what-framework/render' ||
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
- if (existingRenderImport) {
1111
- const existingNames = new Set(
1112
- existingRenderImport.specifiers
1113
- .filter(s => t.isImportSpecifier(s))
1114
- .map(s => s.imported.name)
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
- for (const spec of fgSpecifiers) {
1118
- if (!existingNames.has(spec.imported.name)) {
1119
- existingRenderImport.specifiers.push(spec);
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
- if (coreSpecifiers.length > 0) {
1130
- addCoreImports(path, t, coreSpecifiers);
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
- } else {
1134
- // VDOM mode
1135
- if (!state.needsH) return;
1136
-
1137
- const coreSpecifiers = [
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
- if (state.needsIsland) {
1146
- coreSpecifiers.push(
1147
- t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
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
- const transformed = state.mode === 'fine-grained'
1158
- ? transformElementFineGrained(path, state)
1159
- : transformElementVdom(path, state);
1160
- path.replaceWith(transformed);
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.mode === 'fine-grained'
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);