what-compiler 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-compiler",
3
- "version": "0.1.1",
3
+ "version": "0.2.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.1.0"
25
+ "what-core": "^0.2.0"
26
26
  },
27
27
  "files": [
28
28
  "src"
@@ -1,37 +1,27 @@
1
1
  /**
2
2
  * What Framework Babel Plugin
3
- * Transforms JSX into optimized DOM operations
3
+ * Transforms JSX into h() calls, routing all rendering through core's VNode reconciler.
4
+ *
5
+ * Output: h(tag, props, ...children) from what-core
4
6
  *
5
7
  * Features:
6
- * - Direct DOM creation (no virtual DOM)
7
- * - Signal auto-unwrapping in expressions
8
- * - Event modifiers (onClick|preventDefault)
9
- * - Two-way binding (bind:value)
10
- * - Static content hoisting
11
- * - Control flow component optimization
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
12
17
  */
13
18
 
14
- const CONTROL_FLOW_COMPONENTS = new Set(['Show', 'For', 'Switch', 'Match', 'Suspense', 'ErrorBoundary', 'Portal']);
15
19
  const EVENT_MODIFIERS = new Set(['preventDefault', 'stopPropagation', 'once', 'capture', 'passive', 'self']);
16
- const SVG_ELEMENTS = new Set([
17
- 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse',
18
- 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask', 'pattern', 'image',
19
- 'text', 'tspan', 'textPath', 'foreignObject', 'linearGradient', 'radialGradient', 'stop'
20
- ]);
20
+ const EVENT_OPTION_MODIFIERS = new Set(['once', 'capture', 'passive']);
21
21
 
22
22
  export default function whatBabelPlugin({ types: t }) {
23
- // Helper to check if an expression might be a signal
24
- function mightBeSignal(node) {
25
- // Identifiers that are likely signals
26
- if (t.isIdentifier(node)) return true;
27
- // Member expressions like store.count
28
- if (t.isMemberExpression(node)) return true;
29
- // Call expressions are definitely reactive
30
- if (t.isCallExpression(node)) return true;
31
- return false;
32
- }
33
23
 
34
- // Parse event modifiers from attribute name
24
+ // Parse event modifiers from attribute name like onClick|preventDefault|once
35
25
  function parseEventModifiers(name) {
36
26
  const parts = name.split('|');
37
27
  const eventName = parts[0];
@@ -46,7 +36,7 @@ export default function whatBabelPlugin({ types: t }) {
46
36
 
47
37
  // Get the binding property from bind:value -> value
48
38
  function getBindingProperty(name) {
49
- return name.slice(5); // Remove 'bind:'
39
+ return name.slice(5);
50
40
  }
51
41
 
52
42
  // Check if element is a component (starts with uppercase)
@@ -54,50 +44,23 @@ export default function whatBabelPlugin({ types: t }) {
54
44
  return /^[A-Z]/.test(name);
55
45
  }
56
46
 
57
- // Check if element is SVG
58
- function isSVGElement(name) {
59
- return SVG_ELEMENTS.has(name);
60
- }
61
-
62
- // Transform JSX attribute value to runtime value
63
- function transformAttributeValue(value, isDynamic = false) {
64
- if (!value) {
65
- // Boolean attribute: <input disabled />
66
- return t.booleanLiteral(true);
67
- }
68
-
69
- if (t.isJSXExpressionContainer(value)) {
70
- const expr = value.expression;
71
-
72
- // Wrap potentially reactive expressions
73
- if (isDynamic && mightBeSignal(expr)) {
74
- // () => expr (auto-unwrap at runtime)
75
- return t.arrowFunctionExpression([], expr);
76
- }
77
-
78
- return expr;
79
- }
80
-
81
- if (t.isStringLiteral(value)) {
82
- return value;
83
- }
84
-
47
+ // Get the expression from a JSX attribute value
48
+ function getAttributeValue(value) {
49
+ if (!value) return t.booleanLiteral(true);
50
+ if (t.isJSXExpressionContainer(value)) return value.expression;
51
+ if (t.isStringLiteral(value)) return value;
85
52
  return t.stringLiteral(value.value || '');
86
53
  }
87
54
 
88
- // Create event handler with modifiers
55
+ // Create event handler wrapper for inline modifiers (preventDefault, stopPropagation, self)
89
56
  function createEventHandler(handler, modifiers) {
90
- if (modifiers.length === 0) {
91
- return handler;
92
- }
57
+ if (modifiers.length === 0) return handler;
93
58
 
94
- // Build modifier chain
95
59
  let wrappedHandler = handler;
96
60
 
97
61
  for (const mod of modifiers) {
98
62
  switch (mod) {
99
63
  case 'preventDefault':
100
- // (e) => { e.preventDefault(); handler(e); }
101
64
  wrappedHandler = t.arrowFunctionExpression(
102
65
  [t.identifier('e')],
103
66
  t.blockStatement([
@@ -132,7 +95,6 @@ export default function whatBabelPlugin({ types: t }) {
132
95
  break;
133
96
 
134
97
  case 'self':
135
- // (e) => { if (e.target === e.currentTarget) handler(e); }
136
98
  wrappedHandler = t.arrowFunctionExpression(
137
99
  [t.identifier('e')],
138
100
  t.blockStatement([
@@ -150,11 +112,10 @@ export default function whatBabelPlugin({ types: t }) {
150
112
  );
151
113
  break;
152
114
 
115
+ // once, capture, passive are handled via _eventOpts, not handler wrapping
153
116
  case 'once':
154
117
  case 'capture':
155
118
  case 'passive':
156
- // These are handled at addEventListener level, mark them
157
- // Will be processed by the runtime
158
119
  break;
159
120
  }
160
121
  }
@@ -162,7 +123,51 @@ export default function whatBabelPlugin({ types: t }) {
162
123
  return wrappedHandler;
163
124
  }
164
125
 
165
- // Transform JSX element to createElement call
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
+ }
146
+
147
+ // Transform children array from JSX
148
+ function transformChildren(children, state) {
149
+ const result = [];
150
+ for (const child of children) {
151
+ if (t.isJSXText(child)) {
152
+ const text = child.value.replace(/\n\s+/g, ' ').trim();
153
+ if (text) {
154
+ result.push(t.stringLiteral(text));
155
+ }
156
+ } else if (t.isJSXExpressionContainer(child)) {
157
+ if (!t.isJSXEmptyExpression(child.expression)) {
158
+ result.push(child.expression);
159
+ }
160
+ } else if (t.isJSXElement(child)) {
161
+ result.push(transformElement({ node: child }, state));
162
+ } else if (t.isJSXFragment(child)) {
163
+ // Inline fragment children
164
+ result.push(transformFragment({ node: child }, state));
165
+ }
166
+ }
167
+ return result;
168
+ }
169
+
170
+ // Transform a JSX element to h() call
166
171
  function transformElement(path, state) {
167
172
  const { node } = path;
168
173
  const openingElement = node.openingElement;
@@ -170,17 +175,15 @@ export default function whatBabelPlugin({ types: t }) {
170
175
  const attributes = openingElement.attributes;
171
176
  const children = node.children;
172
177
 
173
- // Check if it's a component
174
178
  if (isComponent(tagName)) {
175
179
  return transformComponent(path, state);
176
180
  }
177
181
 
178
- // Build props object
182
+ // Build props
179
183
  const props = [];
180
- const eventProps = [];
181
- const bindProps = [];
182
184
  let hasSpread = false;
183
185
  let spreadExpr = null;
186
+ const eventOptsStatements = [];
184
187
 
185
188
  for (const attr of attributes) {
186
189
  if (t.isJSXSpreadAttribute(attr)) {
@@ -189,147 +192,155 @@ export default function whatBabelPlugin({ types: t }) {
189
192
  continue;
190
193
  }
191
194
 
192
- const attrName = attr.name.name || attr.name;
195
+ const attrName = typeof attr.name.name === 'string' ? attr.name.name : String(attr.name.name);
193
196
 
194
197
  // Handle event modifiers: onClick|preventDefault
195
198
  if (attrName.startsWith('on') && attrName.includes('|')) {
196
199
  const { eventName, modifiers } = parseEventModifiers(attrName);
197
- const handler = transformAttributeValue(attr.value);
200
+ const handler = getAttributeValue(attr.value);
198
201
  const wrappedHandler = createEventHandler(handler, modifiers);
199
202
 
200
- eventProps.push(
201
- t.objectProperty(
202
- t.stringLiteral(eventName),
203
- t.objectExpression([
204
- t.objectProperty(t.identifier('handler'), wrappedHandler),
205
- t.objectProperty(
206
- t.identifier('modifiers'),
207
- t.arrayExpression(modifiers.map(m => t.stringLiteral(m)))
208
- )
209
- ])
210
- )
211
- );
203
+ // Check if we need _eventOpts (once/capture/passive)
204
+ const optionMods = modifiers.filter(m => EVENT_OPTION_MODIFIERS.has(m));
205
+ if (optionMods.length > 0) {
206
+ // Need a temp variable for the handler to attach _eventOpts
207
+ const tempId = path.scope
208
+ ? path.scope.generateUidIdentifier('handler')
209
+ : t.identifier('_h' + Math.random().toString(36).slice(2, 6));
210
+
211
+ // We'll use an IIFE: (() => { const _h = handler; _h._eventOpts = {...}; return _h; })()
212
+ const optsProps = optionMods.map(m =>
213
+ t.objectProperty(t.identifier(m), t.booleanLiteral(true))
214
+ );
215
+
216
+ const iifeHandler = t.callExpression(
217
+ t.arrowFunctionExpression(
218
+ [],
219
+ t.blockStatement([
220
+ t.variableDeclaration('const', [
221
+ t.variableDeclarator(tempId, wrappedHandler)
222
+ ]),
223
+ t.expressionStatement(
224
+ t.assignmentExpression(
225
+ '=',
226
+ t.memberExpression(t.cloneNode(tempId), t.identifier('_eventOpts')),
227
+ t.objectExpression(optsProps)
228
+ )
229
+ ),
230
+ t.returnStatement(t.cloneNode(tempId))
231
+ ])
232
+ ),
233
+ []
234
+ );
235
+
236
+ props.push(
237
+ t.objectProperty(t.identifier(eventName), iifeHandler)
238
+ );
239
+ } else {
240
+ props.push(
241
+ t.objectProperty(t.identifier(eventName), wrappedHandler)
242
+ );
243
+ }
212
244
  continue;
213
245
  }
214
246
 
215
- // Handle two-way binding: bind:value
247
+ // Handle two-way binding: bind:value={sig}
216
248
  if (isBindingAttribute(attrName)) {
217
249
  const bindProp = getBindingProperty(attrName);
218
- const signal = attr.value.expression;
219
-
220
- bindProps.push(
221
- t.objectProperty(
222
- t.stringLiteral(bindProp),
223
- signal
224
- )
225
- );
250
+ const signalExpr = attr.value.expression;
251
+
252
+ if (bindProp === 'value') {
253
+ // { value: sig(), onInput: (e) => sig.set(e.target.value) }
254
+ props.push(
255
+ t.objectProperty(
256
+ t.identifier('value'),
257
+ t.callExpression(t.cloneNode(signalExpr), [])
258
+ )
259
+ );
260
+ props.push(
261
+ t.objectProperty(
262
+ t.identifier('onInput'),
263
+ t.arrowFunctionExpression(
264
+ [t.identifier('e')],
265
+ t.callExpression(
266
+ t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
267
+ [t.memberExpression(
268
+ t.memberExpression(t.identifier('e'), t.identifier('target')),
269
+ t.identifier('value')
270
+ )]
271
+ )
272
+ )
273
+ )
274
+ );
275
+ } else if (bindProp === 'checked') {
276
+ // { checked: sig(), onChange: (e) => sig.set(e.target.checked) }
277
+ props.push(
278
+ t.objectProperty(
279
+ t.identifier('checked'),
280
+ t.callExpression(t.cloneNode(signalExpr), [])
281
+ )
282
+ );
283
+ props.push(
284
+ t.objectProperty(
285
+ t.identifier('onChange'),
286
+ t.arrowFunctionExpression(
287
+ [t.identifier('e')],
288
+ t.callExpression(
289
+ t.memberExpression(t.cloneNode(signalExpr), t.identifier('set')),
290
+ [t.memberExpression(
291
+ t.memberExpression(t.identifier('e'), t.identifier('target')),
292
+ t.identifier('checked')
293
+ )]
294
+ )
295
+ )
296
+ )
297
+ );
298
+ }
226
299
  continue;
227
300
  }
228
301
 
229
- // Handle regular attributes
230
- const isDynamic = t.isJSXExpressionContainer(attr.value) && mightBeSignal(attr.value?.expression);
231
- const value = transformAttributeValue(attr.value, isDynamic);
302
+ // Regular attributes
303
+ const value = getAttributeValue(attr.value);
232
304
 
233
- // Use the correct attribute name (className -> class for DOM)
305
+ // Normalize className -> class, htmlFor -> for
234
306
  let domAttrName = attrName;
235
307
  if (attrName === 'className') domAttrName = 'class';
236
308
  if (attrName === 'htmlFor') domAttrName = 'for';
237
309
 
238
310
  props.push(
239
311
  t.objectProperty(
240
- t.stringLiteral(domAttrName),
312
+ // Use identifier for valid JS identifiers, string literal otherwise
313
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(domAttrName)
314
+ ? t.identifier(domAttrName)
315
+ : t.stringLiteral(domAttrName),
241
316
  value
242
317
  )
243
318
  );
244
319
  }
245
320
 
246
- // Transform children
247
- const transformedChildren = [];
248
- for (const child of children) {
249
- if (t.isJSXText(child)) {
250
- const text = child.value.trim();
251
- if (text) {
252
- transformedChildren.push(t.stringLiteral(text));
253
- }
254
- } else if (t.isJSXExpressionContainer(child)) {
255
- if (!t.isJSXEmptyExpression(child.expression)) {
256
- const expr = child.expression;
257
- // Wrap signal expressions for reactivity
258
- if (mightBeSignal(expr)) {
259
- transformedChildren.push(
260
- t.arrowFunctionExpression([], expr)
261
- );
262
- } else {
263
- transformedChildren.push(expr);
264
- }
265
- }
266
- } else if (t.isJSXElement(child)) {
267
- // Recursively transform child elements
268
- transformedChildren.push(transformElement({ node: child }, state));
269
- } else if (t.isJSXFragment(child)) {
270
- // Handle fragments
271
- for (const fragChild of child.children) {
272
- if (t.isJSXElement(fragChild)) {
273
- transformedChildren.push(transformElement({ node: fragChild }, state));
274
- }
275
- }
276
- }
277
- }
278
-
279
- // Build the createElement call
280
- const args = [
281
- t.stringLiteral(tagName)
282
- ];
283
-
284
- // Props object
285
- const propsObj = [];
286
- if (props.length > 0) {
287
- propsObj.push(...props);
288
- }
289
- if (eventProps.length > 0) {
290
- propsObj.push(
291
- t.objectProperty(
292
- t.identifier('_events'),
293
- t.objectExpression(eventProps)
294
- )
295
- );
296
- }
297
- if (bindProps.length > 0) {
298
- propsObj.push(
299
- t.objectProperty(
300
- t.identifier('_bindings'),
301
- t.objectExpression(bindProps)
302
- )
303
- );
304
- }
321
+ // Build the h() call: h(tag, props, ...children)
322
+ const transformedChildren = transformChildren(children, state);
305
323
 
324
+ let propsExpr;
306
325
  if (hasSpread) {
307
- // Merge spread with props
308
- args.push(
309
- t.callExpression(
326
+ if (props.length > 0) {
327
+ propsExpr = t.callExpression(
310
328
  t.memberExpression(t.identifier('Object'), t.identifier('assign')),
311
- [t.objectExpression([]), spreadExpr, t.objectExpression(propsObj)]
312
- )
313
- );
329
+ [t.objectExpression([]), spreadExpr, t.objectExpression(props)]
330
+ );
331
+ } else {
332
+ propsExpr = spreadExpr;
333
+ }
334
+ } else if (props.length > 0) {
335
+ propsExpr = t.objectExpression(props);
314
336
  } else {
315
- args.push(t.objectExpression(propsObj));
316
- }
317
-
318
- // Children array
319
- if (transformedChildren.length > 0) {
320
- args.push(t.arrayExpression(transformedChildren));
337
+ propsExpr = t.nullLiteral();
321
338
  }
322
339
 
323
- // Determine if SVG context
324
- const isSvg = isSVGElement(tagName);
325
-
326
- // Call the appropriate createElement function
327
- const createFn = isSvg ? '_createSVGElement' : '_createElement';
340
+ const args = [t.stringLiteral(tagName), propsExpr, ...transformedChildren];
328
341
 
329
- return t.callExpression(
330
- t.identifier(createFn),
331
- args
332
- );
342
+ state.needsH = true;
343
+ return t.callExpression(t.identifier('h'), args);
333
344
  }
334
345
 
335
346
  // Transform component JSX
@@ -348,13 +359,11 @@ export default function whatBabelPlugin({ types: t }) {
348
359
  if (t.isJSXAttribute(attr)) {
349
360
  const name = attr.name.name;
350
361
  if (name && name.startsWith('client:')) {
351
- clientDirective = name.slice(7); // 'load', 'idle', 'visible', etc.
362
+ const mode = name.slice(7); // 'load', 'idle', 'visible', etc.
352
363
  if (attr.value) {
353
- // client:media="(max-width: 768px)"
354
- clientDirective = {
355
- type: clientDirective,
356
- value: attr.value.value
357
- };
364
+ clientDirective = { type: mode, value: attr.value.value };
365
+ } else {
366
+ clientDirective = { type: mode };
358
367
  }
359
368
  continue;
360
369
  }
@@ -362,7 +371,43 @@ export default function whatBabelPlugin({ types: t }) {
362
371
  filteredAttrs.push(attr);
363
372
  }
364
373
 
365
- // Build props object
374
+ // Handle islands — h(Island, { component: Comp, mode: 'idle', ...props })
375
+ if (clientDirective) {
376
+ state.needsH = true;
377
+ state.needsIsland = true;
378
+
379
+ const islandProps = [
380
+ t.objectProperty(t.identifier('component'), t.identifier(componentName)),
381
+ t.objectProperty(
382
+ t.identifier('mode'),
383
+ t.stringLiteral(clientDirective.type)
384
+ ),
385
+ ];
386
+
387
+ if (clientDirective.value) {
388
+ islandProps.push(
389
+ t.objectProperty(
390
+ t.identifier('mediaQuery'),
391
+ t.stringLiteral(clientDirective.value)
392
+ )
393
+ );
394
+ }
395
+
396
+ // Add remaining props
397
+ for (const attr of filteredAttrs) {
398
+ if (t.isJSXSpreadAttribute(attr)) continue;
399
+ const attrName = attr.name.name;
400
+ const value = getAttributeValue(attr.value);
401
+ islandProps.push(t.objectProperty(t.identifier(attrName), value));
402
+ }
403
+
404
+ return t.callExpression(
405
+ t.identifier('h'),
406
+ [t.identifier('Island'), t.objectExpression(islandProps)]
407
+ );
408
+ }
409
+
410
+ // Build props
366
411
  const props = [];
367
412
  let hasSpread = false;
368
413
  let spreadExpr = null;
@@ -375,97 +420,56 @@ export default function whatBabelPlugin({ types: t }) {
375
420
  }
376
421
 
377
422
  const attrName = attr.name.name;
378
- const value = transformAttributeValue(attr.value);
423
+ const value = getAttributeValue(attr.value);
379
424
 
380
425
  props.push(
381
426
  t.objectProperty(
382
- t.identifier(attrName),
427
+ /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(attrName)
428
+ ? t.identifier(attrName)
429
+ : t.stringLiteral(attrName),
383
430
  value
384
431
  )
385
432
  );
386
433
  }
387
434
 
388
- // Handle children
389
- const transformedChildren = [];
390
- for (const child of children) {
391
- if (t.isJSXText(child)) {
392
- const text = child.value.trim();
393
- if (text) {
394
- transformedChildren.push(t.stringLiteral(text));
395
- }
396
- } else if (t.isJSXExpressionContainer(child)) {
397
- if (!t.isJSXEmptyExpression(child.expression)) {
398
- transformedChildren.push(child.expression);
399
- }
400
- } else if (t.isJSXElement(child)) {
401
- transformedChildren.push(transformElement({ node: child }, state));
402
- }
403
- }
404
-
405
- // Add children to props if present
406
- if (transformedChildren.length > 0) {
407
- if (transformedChildren.length === 1) {
408
- props.push(
409
- t.objectProperty(
410
- t.identifier('children'),
411
- transformedChildren[0]
412
- )
413
- );
414
- } else {
415
- props.push(
416
- t.objectProperty(
417
- t.identifier('children'),
418
- t.arrayExpression(transformedChildren)
419
- )
420
- );
421
- }
422
- }
435
+ // Transform children
436
+ const transformedChildren = transformChildren(children, state);
423
437
 
424
438
  // Build props expression
425
439
  let propsExpr;
426
440
  if (hasSpread) {
427
- propsExpr = t.callExpression(
428
- t.memberExpression(t.identifier('Object'), t.identifier('assign')),
429
- [t.objectExpression([]), spreadExpr, t.objectExpression(props)]
430
- );
431
- } else {
441
+ if (props.length > 0) {
442
+ propsExpr = t.callExpression(
443
+ t.memberExpression(t.identifier('Object'), t.identifier('assign')),
444
+ [t.objectExpression([]), spreadExpr, t.objectExpression(props)]
445
+ );
446
+ } else {
447
+ propsExpr = spreadExpr;
448
+ }
449
+ } else if (props.length > 0) {
432
450
  propsExpr = t.objectExpression(props);
451
+ } else {
452
+ propsExpr = t.nullLiteral();
433
453
  }
434
454
 
435
- // Handle control flow components specially
436
- if (CONTROL_FLOW_COMPONENTS.has(componentName)) {
437
- return t.callExpression(
438
- t.identifier(`_${componentName}`),
439
- [propsExpr]
440
- );
441
- }
455
+ // h(Component, props, ...children)
456
+ const args = [t.identifier(componentName), propsExpr, ...transformedChildren];
442
457
 
443
- // Handle islands
444
- if (clientDirective) {
445
- const directiveValue = typeof clientDirective === 'object'
446
- ? t.objectExpression([
447
- t.objectProperty(t.identifier('type'), t.stringLiteral(clientDirective.type)),
448
- t.objectProperty(t.identifier('value'), t.stringLiteral(clientDirective.value))
449
- ])
450
- : t.stringLiteral(clientDirective);
458
+ state.needsH = true;
459
+ return t.callExpression(t.identifier('h'), args);
460
+ }
451
461
 
452
- return t.callExpression(
453
- t.identifier('_createIsland'),
454
- [
455
- t.identifier(componentName),
456
- propsExpr,
457
- directiveValue
458
- ]
459
- );
460
- }
462
+ // Transform JSX fragment to h(Fragment, null, ...children)
463
+ function transformFragment(path, state) {
464
+ const { node } = path;
465
+ const transformedChildren = transformChildren(node.children, state);
466
+
467
+ state.needsH = true;
468
+ state.needsFragment = true;
461
469
 
462
- // Regular component call
463
470
  return t.callExpression(
464
- t.identifier('_createComponent'),
465
- [
466
- t.identifier(componentName),
467
- propsExpr
468
- ]
471
+ t.identifier('h'),
472
+ [t.identifier('Fragment'), t.nullLiteral(), ...transformedChildren]
469
473
  );
470
474
  }
471
475
 
@@ -475,60 +479,80 @@ export default function whatBabelPlugin({ types: t }) {
475
479
  visitor: {
476
480
  Program: {
477
481
  enter(path, state) {
478
- state.needsRuntime = false;
479
- state.usedHelpers = new Set();
482
+ state.needsH = false;
483
+ state.needsFragment = false;
484
+ state.needsIsland = false;
480
485
  },
481
486
 
482
487
  exit(path, state) {
483
- if (state.needsRuntime) {
484
- // Add runtime imports at the top
485
- const helpers = Array.from(state.usedHelpers);
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-core')
508
+ );
509
+
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;
516
+ }
517
+ }
518
+
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
+ }
526
+ }
527
+ }
486
528
 
487
- const importDecl = t.importDeclaration(
488
- helpers.map(h => t.importSpecifier(t.identifier(h), t.identifier(h))),
489
- t.stringLiteral('what-compiler/runtime')
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)
490
535
  );
491
536
 
537
+ for (const spec of coreSpecifiers) {
538
+ if (!existingNames.has(spec.imported.name)) {
539
+ existingImport.specifiers.push(spec);
540
+ }
541
+ }
542
+ } else {
492
543
  path.unshiftContainer('body', importDecl);
493
544
  }
494
545
  }
495
546
  },
496
547
 
497
548
  JSXElement(path, state) {
498
- state.needsRuntime = true;
499
- state.usedHelpers.add('_createElement');
500
- state.usedHelpers.add('_createComponent');
501
-
502
549
  const transformed = transformElement(path, state);
503
550
  path.replaceWith(transformed);
504
551
  },
505
552
 
506
553
  JSXFragment(path, state) {
507
- state.needsRuntime = true;
508
- state.usedHelpers.add('_Fragment');
509
-
510
- const children = [];
511
- for (const child of path.node.children) {
512
- if (t.isJSXText(child)) {
513
- const text = child.value.trim();
514
- if (text) {
515
- children.push(t.stringLiteral(text));
516
- }
517
- } else if (t.isJSXExpressionContainer(child)) {
518
- if (!t.isJSXEmptyExpression(child.expression)) {
519
- children.push(child.expression);
520
- }
521
- } else if (t.isJSXElement(child)) {
522
- children.push(transformElement({ node: child }, state));
523
- }
524
- }
525
-
526
- path.replaceWith(
527
- t.callExpression(
528
- t.identifier('_Fragment'),
529
- [t.arrayExpression(children)]
530
- )
531
- );
554
+ const transformed = transformFragment(path, state);
555
+ path.replaceWith(transformed);
532
556
  }
533
557
  }
534
558
  };
package/src/runtime.js CHANGED
@@ -1,483 +1,11 @@
1
1
  /**
2
- * What Framework Runtime
3
- * Optimized DOM creation and updates with signal reactivity
2
+ * What Compiler Runtime
3
+ *
4
+ * With the unified rendering path (babel plugin → h() → core reconciler),
5
+ * most runtime helpers are no longer needed. The compiler now outputs h() calls
6
+ * that go through what-core's VNode reconciler.
7
+ *
8
+ * This file re-exports from what-core for backwards compatibility.
4
9
  */
5
10
 
6
- import { effect, untrack, batch } from 'what-core';
7
-
8
- // Check if value is a signal (callable with .set)
9
- function isSignal(value) {
10
- return typeof value === 'function' && typeof value.set === 'function';
11
- }
12
-
13
- // Unwrap a potentially reactive value
14
- function unwrap(value) {
15
- if (typeof value === 'function') {
16
- return value();
17
- }
18
- return value;
19
- }
20
-
21
- // Set a DOM property with proper handling
22
- function setProperty(el, key, value, isSVG = false) {
23
- if (key === 'class') {
24
- el.className = value ?? '';
25
- } else if (key === 'style') {
26
- if (typeof value === 'string') {
27
- el.style.cssText = value;
28
- } else if (typeof value === 'object') {
29
- Object.assign(el.style, value);
30
- }
31
- } else if (key === 'ref') {
32
- if (typeof value === 'function') {
33
- value(el);
34
- } else if (value && typeof value === 'object') {
35
- value.current = el;
36
- }
37
- } else if (key.startsWith('on') && typeof value === 'function') {
38
- const eventName = key.slice(2).toLowerCase();
39
- el.addEventListener(eventName, value);
40
- } else if (key === 'innerHTML' || key === 'textContent') {
41
- el[key] = value ?? '';
42
- } else if (key in el && !isSVG) {
43
- try {
44
- el[key] = value;
45
- } catch {
46
- el.setAttribute(key, value);
47
- }
48
- } else {
49
- if (value === true) {
50
- el.setAttribute(key, '');
51
- } else if (value === false || value == null) {
52
- el.removeAttribute(key);
53
- } else {
54
- el.setAttribute(key, value);
55
- }
56
- }
57
- }
58
-
59
- // Create a reactive property binding
60
- function bindProperty(el, key, getValue, isSVG = false) {
61
- let currentValue;
62
-
63
- effect(() => {
64
- const newValue = unwrap(getValue);
65
- if (newValue !== currentValue) {
66
- currentValue = newValue;
67
- setProperty(el, key, newValue, isSVG);
68
- }
69
- });
70
- }
71
-
72
- // Create a text node that updates reactively
73
- function createReactiveText(getValue) {
74
- const node = document.createTextNode('');
75
- let currentValue;
76
-
77
- effect(() => {
78
- const newValue = String(unwrap(getValue) ?? '');
79
- if (newValue !== currentValue) {
80
- currentValue = newValue;
81
- node.textContent = newValue;
82
- }
83
- });
84
-
85
- return node;
86
- }
87
-
88
- /**
89
- * Create a DOM element with optimized property handling
90
- */
91
- export function _createElement(tag, props = {}, children = []) {
92
- const el = document.createElement(tag);
93
- const isSVG = false;
94
-
95
- // Process props
96
- for (const [key, value] of Object.entries(props)) {
97
- if (key === '_events') {
98
- // Handle events with modifiers
99
- for (const [eventKey, eventConfig] of Object.entries(value)) {
100
- const eventName = eventKey.slice(2).toLowerCase();
101
- const { handler, modifiers } = eventConfig;
102
-
103
- const options = {};
104
- if (modifiers.includes('capture')) options.capture = true;
105
- if (modifiers.includes('passive')) options.passive = true;
106
- if (modifiers.includes('once')) options.once = true;
107
-
108
- el.addEventListener(eventName, handler, options);
109
- }
110
- continue;
111
- }
112
-
113
- if (key === '_bindings') {
114
- // Handle two-way bindings
115
- for (const [bindKey, signal] of Object.entries(value)) {
116
- if (bindKey === 'value') {
117
- // Input value binding
118
- el.value = unwrap(signal);
119
-
120
- effect(() => {
121
- const newValue = unwrap(signal);
122
- if (el.value !== newValue) {
123
- el.value = newValue;
124
- }
125
- });
126
-
127
- el.addEventListener('input', (e) => {
128
- signal.set(e.target.value);
129
- });
130
- } else if (bindKey === 'checked') {
131
- // Checkbox binding
132
- el.checked = unwrap(signal);
133
-
134
- effect(() => {
135
- el.checked = unwrap(signal);
136
- });
137
-
138
- el.addEventListener('change', (e) => {
139
- signal.set(e.target.checked);
140
- });
141
- }
142
- }
143
- continue;
144
- }
145
-
146
- // Check if value is reactive (a function)
147
- if (typeof value === 'function' && !key.startsWith('on')) {
148
- bindProperty(el, key, value, isSVG);
149
- } else {
150
- setProperty(el, key, value, isSVG);
151
- }
152
- }
153
-
154
- // Process children
155
- for (const child of children) {
156
- appendChild(el, child);
157
- }
158
-
159
- return el;
160
- }
161
-
162
- /**
163
- * Create an SVG element
164
- */
165
- export function _createSVGElement(tag, props = {}, children = []) {
166
- const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
167
- const isSVG = true;
168
-
169
- // Process props
170
- for (const [key, value] of Object.entries(props)) {
171
- if (key === '_events' || key === '_bindings') continue;
172
-
173
- if (typeof value === 'function' && !key.startsWith('on')) {
174
- bindProperty(el, key, value, isSVG);
175
- } else {
176
- setProperty(el, key, value, isSVG);
177
- }
178
- }
179
-
180
- // Process children
181
- for (const child of children) {
182
- appendChild(el, child);
183
- }
184
-
185
- return el;
186
- }
187
-
188
- /**
189
- * Append a child to an element, handling different types
190
- */
191
- function appendChild(parent, child) {
192
- if (child == null || child === false || child === true) {
193
- return;
194
- }
195
-
196
- if (typeof child === 'string' || typeof child === 'number') {
197
- parent.appendChild(document.createTextNode(String(child)));
198
- } else if (typeof child === 'function') {
199
- // Reactive child - could be a signal or computed
200
- parent.appendChild(createReactiveText(child));
201
- } else if (child instanceof Node) {
202
- parent.appendChild(child);
203
- } else if (Array.isArray(child)) {
204
- for (const c of child) {
205
- appendChild(parent, c);
206
- }
207
- }
208
- }
209
-
210
- /**
211
- * Create a component instance
212
- */
213
- export function _createComponent(Component, props) {
214
- // Components are just functions that return DOM nodes
215
- return untrack(() => Component(props));
216
- }
217
-
218
- /**
219
- * Create a fragment (array of nodes)
220
- */
221
- export function _Fragment(children) {
222
- const fragment = document.createDocumentFragment();
223
- for (const child of children) {
224
- appendChild(fragment, child);
225
- }
226
- return fragment;
227
- }
228
-
229
- /**
230
- * Show component - conditional rendering
231
- */
232
- export function _Show(props) {
233
- const { when, fallback, children } = props;
234
- const marker = document.createComment('Show');
235
- let currentNodes = [];
236
- let showingMain = null;
237
-
238
- const container = document.createDocumentFragment();
239
- container.appendChild(marker);
240
-
241
- effect(() => {
242
- const condition = unwrap(when);
243
- const shouldShowMain = Boolean(condition);
244
-
245
- if (shouldShowMain === showingMain) return;
246
- showingMain = shouldShowMain;
247
-
248
- // Remove current nodes
249
- for (const node of currentNodes) {
250
- node.parentNode?.removeChild(node);
251
- }
252
- currentNodes = [];
253
-
254
- // Insert new nodes
255
- const content = shouldShowMain ? children : fallback;
256
- if (content != null) {
257
- const nodes = Array.isArray(content) ? content : [content];
258
- for (const node of nodes) {
259
- if (node instanceof Node) {
260
- marker.parentNode?.insertBefore(node, marker.nextSibling);
261
- currentNodes.push(node);
262
- } else if (typeof node === 'string') {
263
- const textNode = document.createTextNode(node);
264
- marker.parentNode?.insertBefore(textNode, marker.nextSibling);
265
- currentNodes.push(textNode);
266
- }
267
- }
268
- }
269
- });
270
-
271
- return container;
272
- }
273
-
274
- /**
275
- * For component - list rendering
276
- */
277
- export function _For(props) {
278
- const { each, children: renderFn } = props;
279
- const marker = document.createComment('For');
280
- let currentNodes = [];
281
- let currentItems = [];
282
-
283
- const container = document.createDocumentFragment();
284
- container.appendChild(marker);
285
-
286
- effect(() => {
287
- const items = unwrap(each) || [];
288
-
289
- // Simple reconciliation - replace all for now
290
- // TODO: Keyed reconciliation for better performance
291
- for (const node of currentNodes) {
292
- node.parentNode?.removeChild(node);
293
- }
294
- currentNodes = [];
295
-
296
- const parent = marker.parentNode;
297
- if (!parent) return;
298
-
299
- items.forEach((item, index) => {
300
- const rendered = renderFn(item, index);
301
- if (rendered instanceof Node) {
302
- parent.insertBefore(rendered, marker);
303
- currentNodes.push(rendered);
304
- }
305
- });
306
-
307
- currentItems = items;
308
- });
309
-
310
- return container;
311
- }
312
-
313
- /**
314
- * Switch/Match components
315
- */
316
- export function _Switch(props) {
317
- const { children } = props;
318
- const matches = Array.isArray(children) ? children : [children];
319
-
320
- const marker = document.createComment('Switch');
321
- let currentNode = null;
322
-
323
- const container = document.createDocumentFragment();
324
- container.appendChild(marker);
325
-
326
- effect(() => {
327
- // Find first matching condition
328
- let matched = null;
329
- for (const match of matches) {
330
- if (match && match._isMatch && unwrap(match.when)) {
331
- matched = match.children;
332
- break;
333
- }
334
- }
335
-
336
- // Remove current
337
- if (currentNode) {
338
- currentNode.parentNode?.removeChild(currentNode);
339
- currentNode = null;
340
- }
341
-
342
- // Insert matched
343
- if (matched instanceof Node) {
344
- marker.parentNode?.insertBefore(matched, marker.nextSibling);
345
- currentNode = matched;
346
- }
347
- });
348
-
349
- return container;
350
- }
351
-
352
- export function _Match(props) {
353
- return {
354
- _isMatch: true,
355
- when: props.when,
356
- children: props.children
357
- };
358
- }
359
-
360
- /**
361
- * Create an island (deferred hydration)
362
- */
363
- export function _createIsland(Component, props, directive) {
364
- const type = typeof directive === 'string' ? directive : directive.type;
365
- const value = typeof directive === 'object' ? directive.value : null;
366
-
367
- // Create placeholder element
368
- const el = document.createElement('div');
369
- el.setAttribute('data-island', Component.name || 'Island');
370
- el.setAttribute('data-hydrate', type);
371
- if (value) {
372
- el.setAttribute('data-hydrate-value', value);
373
- }
374
-
375
- // Store props for hydration
376
- el._islandProps = props;
377
- el._islandComponent = Component;
378
-
379
- // Schedule hydration based on directive
380
- switch (type) {
381
- case 'load':
382
- // Hydrate immediately
383
- queueMicrotask(() => hydrateIsland(el));
384
- break;
385
-
386
- case 'idle':
387
- // Hydrate when browser is idle
388
- if ('requestIdleCallback' in window) {
389
- requestIdleCallback(() => hydrateIsland(el));
390
- } else {
391
- setTimeout(() => hydrateIsland(el), 200);
392
- }
393
- break;
394
-
395
- case 'visible':
396
- // Hydrate when visible
397
- const observer = new IntersectionObserver((entries) => {
398
- if (entries[0].isIntersecting) {
399
- observer.disconnect();
400
- hydrateIsland(el);
401
- }
402
- });
403
- queueMicrotask(() => observer.observe(el));
404
- break;
405
-
406
- case 'interaction':
407
- // Hydrate on first interaction
408
- const hydrate = () => {
409
- el.removeEventListener('click', hydrate);
410
- el.removeEventListener('focus', hydrate);
411
- el.removeEventListener('mouseenter', hydrate);
412
- hydrateIsland(el);
413
- };
414
- el.addEventListener('click', hydrate, { once: true });
415
- el.addEventListener('focus', hydrate, { once: true });
416
- el.addEventListener('mouseenter', hydrate, { once: true });
417
- break;
418
-
419
- case 'media':
420
- // Hydrate when media query matches
421
- const mq = window.matchMedia(value);
422
- const checkMedia = () => {
423
- if (mq.matches) {
424
- mq.removeEventListener('change', checkMedia);
425
- hydrateIsland(el);
426
- }
427
- };
428
- if (mq.matches) {
429
- queueMicrotask(() => hydrateIsland(el));
430
- } else {
431
- mq.addEventListener('change', checkMedia);
432
- }
433
- break;
434
- }
435
-
436
- return el;
437
- }
438
-
439
- function hydrateIsland(el) {
440
- const Component = el._islandComponent;
441
- const props = el._islandProps;
442
-
443
- if (!Component) return;
444
-
445
- const rendered = _createComponent(Component, props);
446
- if (rendered instanceof Node) {
447
- el.replaceWith(rendered);
448
- }
449
-
450
- // Dispatch event for tracking
451
- document.dispatchEvent(new CustomEvent('island:hydrated', {
452
- detail: { component: Component.name }
453
- }));
454
- }
455
-
456
- /**
457
- * Mount a component to the DOM
458
- */
459
- export function mount(component, container) {
460
- const target = typeof container === 'string'
461
- ? document.querySelector(container)
462
- : container;
463
-
464
- if (!target) {
465
- throw new Error(`Mount target not found: ${container}`);
466
- }
467
-
468
- // Clear container
469
- target.innerHTML = '';
470
-
471
- // Render component
472
- const rendered = typeof component === 'function'
473
- ? component()
474
- : component;
475
-
476
- if (rendered instanceof Node) {
477
- target.appendChild(rendered);
478
- }
479
-
480
- return () => {
481
- target.innerHTML = '';
482
- };
483
- }
11
+ export { h, Fragment, mount, Island } from 'what-core';
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * What Framework Vite Plugin
3
- * Enables JSX transformation and other optimizations
3
+ * Enables JSX transformation via the What babel plugin.
4
+ * JSX is compiled to h() calls that go through what-core's VNode reconciler.
4
5
  */
5
6
 
6
7
  import { transformSync } from '@babel/core';
@@ -57,10 +58,8 @@ export default function whatVitePlugin(options = {}) {
57
58
  config(config, { mode }) {
58
59
  return {
59
60
  esbuild: {
60
- // Let our plugin handle JSX, not esbuild
61
+ // Preserve JSX so our babel plugin handles it don't let esbuild transform it
61
62
  jsx: 'preserve',
62
- jsxFactory: '_createElement',
63
- jsxFragment: '_Fragment'
64
63
  },
65
64
  optimizeDeps: {
66
65
  // Pre-bundle the framework