what-compiler 0.1.2 → 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 +2 -2
- package/src/babel-plugin.js +315 -291
- package/src/runtime.js +8 -534
- package/src/vite-plugin.js +3 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-compiler",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
25
|
+
"what-core": "^0.2.0"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src"
|
package/src/babel-plugin.js
CHANGED
|
@@ -1,37 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What Framework Babel Plugin
|
|
3
|
-
* Transforms JSX into
|
|
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
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
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
|
|
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);
|
|
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
|
-
//
|
|
58
|
-
function
|
|
59
|
-
return
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 =
|
|
200
|
+
const handler = getAttributeValue(attr.value);
|
|
198
201
|
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
//
|
|
230
|
-
const
|
|
231
|
-
const value = transformAttributeValue(attr.value, isDynamic);
|
|
302
|
+
// Regular attributes
|
|
303
|
+
const value = getAttributeValue(attr.value);
|
|
232
304
|
|
|
233
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
308
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
362
|
+
const mode = name.slice(7); // 'load', 'idle', 'visible', etc.
|
|
352
363
|
if (attr.value) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
//
|
|
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 =
|
|
423
|
+
const value = getAttributeValue(attr.value);
|
|
379
424
|
|
|
380
425
|
props.push(
|
|
381
426
|
t.objectProperty(
|
|
382
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
//
|
|
436
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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('
|
|
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.
|
|
479
|
-
state.
|
|
482
|
+
state.needsH = false;
|
|
483
|
+
state.needsFragment = false;
|
|
484
|
+
state.needsIsland = false;
|
|
480
485
|
},
|
|
481
486
|
|
|
482
487
|
exit(path, state) {
|
|
483
|
-
if (state.
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
508
|
-
|
|
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,537 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
-
} else if (child && typeof child === 'object' && child.tag != null) {
|
|
208
|
-
// VNode from h() — interop with what-core's VNode system
|
|
209
|
-
parent.appendChild(renderVNode(child));
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Convert a what-core VNode to a real DOM node (interop bridge)
|
|
214
|
-
function renderVNode(vnode) {
|
|
215
|
-
if (vnode == null || vnode === false || vnode === true) return document.createComment('');
|
|
216
|
-
if (typeof vnode === 'string' || typeof vnode === 'number') return document.createTextNode(String(vnode));
|
|
217
|
-
if (vnode instanceof Node) return vnode;
|
|
218
|
-
if (Array.isArray(vnode)) {
|
|
219
|
-
const frag = document.createDocumentFragment();
|
|
220
|
-
for (const v of vnode) frag.appendChild(renderVNode(v));
|
|
221
|
-
return frag;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const { tag, props, children } = vnode;
|
|
225
|
-
|
|
226
|
-
// Component VNode
|
|
227
|
-
if (typeof tag === 'function') {
|
|
228
|
-
const result = tag({ ...props, children });
|
|
229
|
-
return renderVNode(result);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Element VNode
|
|
233
|
-
const el = document.createElement(tag);
|
|
234
|
-
if (props) {
|
|
235
|
-
for (const [key, val] of Object.entries(props)) {
|
|
236
|
-
if (key === 'key' || key === 'ref' || key === 'children') continue;
|
|
237
|
-
if (key.startsWith('on') && typeof val === 'function') {
|
|
238
|
-
el.addEventListener(key.slice(2).toLowerCase(), val);
|
|
239
|
-
} else if (key === 'className' || key === 'class') {
|
|
240
|
-
el.className = val || '';
|
|
241
|
-
} else if (key === 'style' && typeof val === 'object') {
|
|
242
|
-
Object.assign(el.style, val);
|
|
243
|
-
} else if (val !== false && val != null) {
|
|
244
|
-
el.setAttribute(key, val === true ? '' : String(val));
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (children) {
|
|
249
|
-
for (const child of children) {
|
|
250
|
-
el.appendChild(renderVNode(child));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return el;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Create a component instance
|
|
258
|
-
*/
|
|
259
|
-
export function _createComponent(Component, props) {
|
|
260
|
-
// Call the component function.
|
|
261
|
-
// Don't untrack — framework components like Router need signal tracking.
|
|
262
|
-
const result = Component(props);
|
|
263
|
-
|
|
264
|
-
// If the component returned a VNode (from h()), convert to DOM
|
|
265
|
-
if (result && typeof result === 'object' && !(result instanceof Node) && !Array.isArray(result) && result.tag != null) {
|
|
266
|
-
return renderVNode(result);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Create a fragment (array of nodes)
|
|
274
|
-
*/
|
|
275
|
-
export function _Fragment(children) {
|
|
276
|
-
const fragment = document.createDocumentFragment();
|
|
277
|
-
for (const child of children) {
|
|
278
|
-
appendChild(fragment, child);
|
|
279
|
-
}
|
|
280
|
-
return fragment;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Show component - conditional rendering
|
|
285
|
-
*/
|
|
286
|
-
export function _Show(props) {
|
|
287
|
-
const { when, fallback, children } = props;
|
|
288
|
-
const marker = document.createComment('Show');
|
|
289
|
-
let currentNodes = [];
|
|
290
|
-
let showingMain = null;
|
|
291
|
-
|
|
292
|
-
const container = document.createDocumentFragment();
|
|
293
|
-
container.appendChild(marker);
|
|
294
|
-
|
|
295
|
-
effect(() => {
|
|
296
|
-
const condition = unwrap(when);
|
|
297
|
-
const shouldShowMain = Boolean(condition);
|
|
298
|
-
|
|
299
|
-
if (shouldShowMain === showingMain) return;
|
|
300
|
-
showingMain = shouldShowMain;
|
|
301
|
-
|
|
302
|
-
// Remove current nodes
|
|
303
|
-
for (const node of currentNodes) {
|
|
304
|
-
node.parentNode?.removeChild(node);
|
|
305
|
-
}
|
|
306
|
-
currentNodes = [];
|
|
307
|
-
|
|
308
|
-
// Insert new nodes
|
|
309
|
-
const content = shouldShowMain ? children : fallback;
|
|
310
|
-
if (content != null) {
|
|
311
|
-
const nodes = Array.isArray(content) ? content : [content];
|
|
312
|
-
for (const node of nodes) {
|
|
313
|
-
if (node instanceof Node) {
|
|
314
|
-
marker.parentNode?.insertBefore(node, marker.nextSibling);
|
|
315
|
-
currentNodes.push(node);
|
|
316
|
-
} else if (typeof node === 'string') {
|
|
317
|
-
const textNode = document.createTextNode(node);
|
|
318
|
-
marker.parentNode?.insertBefore(textNode, marker.nextSibling);
|
|
319
|
-
currentNodes.push(textNode);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
return container;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* For component - list rendering
|
|
330
|
-
*/
|
|
331
|
-
export function _For(props) {
|
|
332
|
-
const { each, children: renderFn } = props;
|
|
333
|
-
const marker = document.createComment('For');
|
|
334
|
-
let currentNodes = [];
|
|
335
|
-
let currentItems = [];
|
|
336
|
-
|
|
337
|
-
const container = document.createDocumentFragment();
|
|
338
|
-
container.appendChild(marker);
|
|
339
|
-
|
|
340
|
-
effect(() => {
|
|
341
|
-
const items = unwrap(each) || [];
|
|
342
|
-
|
|
343
|
-
// Simple reconciliation - replace all for now
|
|
344
|
-
// TODO: Keyed reconciliation for better performance
|
|
345
|
-
for (const node of currentNodes) {
|
|
346
|
-
node.parentNode?.removeChild(node);
|
|
347
|
-
}
|
|
348
|
-
currentNodes = [];
|
|
349
|
-
|
|
350
|
-
const parent = marker.parentNode;
|
|
351
|
-
if (!parent) return;
|
|
352
|
-
|
|
353
|
-
items.forEach((item, index) => {
|
|
354
|
-
const rendered = renderFn(item, index);
|
|
355
|
-
if (rendered instanceof Node) {
|
|
356
|
-
parent.insertBefore(rendered, marker);
|
|
357
|
-
currentNodes.push(rendered);
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
currentItems = items;
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
return container;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Switch/Match components
|
|
369
|
-
*/
|
|
370
|
-
export function _Switch(props) {
|
|
371
|
-
const { children } = props;
|
|
372
|
-
const matches = Array.isArray(children) ? children : [children];
|
|
373
|
-
|
|
374
|
-
const marker = document.createComment('Switch');
|
|
375
|
-
let currentNode = null;
|
|
376
|
-
|
|
377
|
-
const container = document.createDocumentFragment();
|
|
378
|
-
container.appendChild(marker);
|
|
379
|
-
|
|
380
|
-
effect(() => {
|
|
381
|
-
// Find first matching condition
|
|
382
|
-
let matched = null;
|
|
383
|
-
for (const match of matches) {
|
|
384
|
-
if (match && match._isMatch && unwrap(match.when)) {
|
|
385
|
-
matched = match.children;
|
|
386
|
-
break;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Remove current
|
|
391
|
-
if (currentNode) {
|
|
392
|
-
currentNode.parentNode?.removeChild(currentNode);
|
|
393
|
-
currentNode = null;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Insert matched
|
|
397
|
-
if (matched instanceof Node) {
|
|
398
|
-
marker.parentNode?.insertBefore(matched, marker.nextSibling);
|
|
399
|
-
currentNode = matched;
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
return container;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export function _Match(props) {
|
|
407
|
-
return {
|
|
408
|
-
_isMatch: true,
|
|
409
|
-
when: props.when,
|
|
410
|
-
children: props.children
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Create an island (deferred hydration)
|
|
416
|
-
*/
|
|
417
|
-
export function _createIsland(Component, props, directive) {
|
|
418
|
-
const type = typeof directive === 'string' ? directive : directive.type;
|
|
419
|
-
const value = typeof directive === 'object' ? directive.value : null;
|
|
420
|
-
|
|
421
|
-
// Create placeholder element
|
|
422
|
-
const el = document.createElement('div');
|
|
423
|
-
el.setAttribute('data-island', Component.name || 'Island');
|
|
424
|
-
el.setAttribute('data-hydrate', type);
|
|
425
|
-
if (value) {
|
|
426
|
-
el.setAttribute('data-hydrate-value', value);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Store props for hydration
|
|
430
|
-
el._islandProps = props;
|
|
431
|
-
el._islandComponent = Component;
|
|
432
|
-
|
|
433
|
-
// Schedule hydration based on directive
|
|
434
|
-
switch (type) {
|
|
435
|
-
case 'load':
|
|
436
|
-
// Hydrate immediately
|
|
437
|
-
queueMicrotask(() => hydrateIsland(el));
|
|
438
|
-
break;
|
|
439
|
-
|
|
440
|
-
case 'idle':
|
|
441
|
-
// Hydrate when browser is idle
|
|
442
|
-
if ('requestIdleCallback' in window) {
|
|
443
|
-
requestIdleCallback(() => hydrateIsland(el));
|
|
444
|
-
} else {
|
|
445
|
-
setTimeout(() => hydrateIsland(el), 200);
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
|
|
449
|
-
case 'visible':
|
|
450
|
-
// Hydrate when visible
|
|
451
|
-
const observer = new IntersectionObserver((entries) => {
|
|
452
|
-
if (entries[0].isIntersecting) {
|
|
453
|
-
observer.disconnect();
|
|
454
|
-
hydrateIsland(el);
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
queueMicrotask(() => observer.observe(el));
|
|
458
|
-
break;
|
|
459
|
-
|
|
460
|
-
case 'interaction':
|
|
461
|
-
// Hydrate on first interaction
|
|
462
|
-
const hydrate = () => {
|
|
463
|
-
el.removeEventListener('click', hydrate);
|
|
464
|
-
el.removeEventListener('focus', hydrate);
|
|
465
|
-
el.removeEventListener('mouseenter', hydrate);
|
|
466
|
-
hydrateIsland(el);
|
|
467
|
-
};
|
|
468
|
-
el.addEventListener('click', hydrate, { once: true });
|
|
469
|
-
el.addEventListener('focus', hydrate, { once: true });
|
|
470
|
-
el.addEventListener('mouseenter', hydrate, { once: true });
|
|
471
|
-
break;
|
|
472
|
-
|
|
473
|
-
case 'media':
|
|
474
|
-
// Hydrate when media query matches
|
|
475
|
-
const mq = window.matchMedia(value);
|
|
476
|
-
const checkMedia = () => {
|
|
477
|
-
if (mq.matches) {
|
|
478
|
-
mq.removeEventListener('change', checkMedia);
|
|
479
|
-
hydrateIsland(el);
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
if (mq.matches) {
|
|
483
|
-
queueMicrotask(() => hydrateIsland(el));
|
|
484
|
-
} else {
|
|
485
|
-
mq.addEventListener('change', checkMedia);
|
|
486
|
-
}
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return el;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function hydrateIsland(el) {
|
|
494
|
-
const Component = el._islandComponent;
|
|
495
|
-
const props = el._islandProps;
|
|
496
|
-
|
|
497
|
-
if (!Component) return;
|
|
498
|
-
|
|
499
|
-
const rendered = _createComponent(Component, props);
|
|
500
|
-
if (rendered instanceof Node) {
|
|
501
|
-
el.replaceWith(rendered);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Dispatch event for tracking
|
|
505
|
-
document.dispatchEvent(new CustomEvent('island:hydrated', {
|
|
506
|
-
detail: { component: Component.name }
|
|
507
|
-
}));
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Mount a component to the DOM
|
|
512
|
-
*/
|
|
513
|
-
export function mount(component, container) {
|
|
514
|
-
const target = typeof container === 'string'
|
|
515
|
-
? document.querySelector(container)
|
|
516
|
-
: container;
|
|
517
|
-
|
|
518
|
-
if (!target) {
|
|
519
|
-
throw new Error(`Mount target not found: ${container}`);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// Clear container
|
|
523
|
-
target.innerHTML = '';
|
|
524
|
-
|
|
525
|
-
// Render component
|
|
526
|
-
const rendered = typeof component === 'function'
|
|
527
|
-
? component()
|
|
528
|
-
: component;
|
|
529
|
-
|
|
530
|
-
if (rendered instanceof Node) {
|
|
531
|
-
target.appendChild(rendered);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
return () => {
|
|
535
|
-
target.innerHTML = '';
|
|
536
|
-
};
|
|
537
|
-
}
|
|
11
|
+
export { h, Fragment, mount, Island } from 'what-core';
|
package/src/vite-plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* What Framework Vite Plugin
|
|
3
|
-
* Enables JSX transformation
|
|
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
|
-
//
|
|
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
|