what-compiler 0.3.0 → 0.4.0

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