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.
@@ -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,21 +640,40 @@ 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
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
- // Direct addEventListener
686
- statements.push(
687
- t.expressionStatement(
688
- t.callExpression(
689
- t.memberExpression(t.identifier(elId), t.identifier('addEventListener')),
690
- [t.stringLiteral(event), handler]
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
- statements.push(
776
- t.expressionStatement(
777
- t.callExpression(t.identifier('_$effect'), [
778
- t.arrowFunctionExpression([], buildSetPropCall(domName, expr))
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
- statements.push(
812
- t.expressionStatement(
813
- t.callExpression(t.identifier('_$insert'), [
814
- t.identifier(elId),
815
- t.arrowFunctionExpression([], expr),
816
- marker
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.name?.name?.startsWith('on')) ||
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 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
- );
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
- // 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);
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.name.name === 'each') {
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
- // Fallback
941
- return transformElementVdom(path, state);
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
- return transformElementVdom(path, state);
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
- // Still uses h(Show, ...) for now — Show is a runtime component
968
- return transformElementVdom(path, state);
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
- function generateTemplateId(state) {
997
- return `_t$${state.templateCount++}`;
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
- 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
-
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
- }
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
- 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;
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
- if (existingRenderImport) {
1111
- const existingNames = new Set(
1112
- existingRenderImport.specifiers
1113
- .filter(s => t.isImportSpecifier(s))
1114
- .map(s => s.imported.name)
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
- for (const spec of fgSpecifiers) {
1118
- if (!existingNames.has(spec.imported.name)) {
1119
- existingRenderImport.specifiers.push(spec);
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
- if (coreSpecifiers.length > 0) {
1130
- addCoreImports(path, t, coreSpecifiers);
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
- } 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'))
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
- if (state.needsIsland) {
1146
- coreSpecifiers.push(
1147
- t.importSpecifier(t.identifier('Island'), t.identifier('Island'))
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
- const transformed = state.mode === 'fine-grained'
1158
- ? transformElementFineGrained(path, state)
1159
- : transformElementVdom(path, state);
1160
- path.replaceWith(transformed);
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.mode === 'fine-grained'
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);