what-compiler 0.1.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 +32 -0
- package/src/babel-plugin.js +535 -0
- package/src/index.js +8 -0
- package/src/runtime.js +483 -0
- package/src/vite-plugin.js +92 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "what-compiler",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "JSX compiler for What Framework - transforms JSX to optimized DOM operations",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./babel": "./src/babel-plugin.js",
|
|
10
|
+
"./vite": "./src/vite-plugin.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"what",
|
|
14
|
+
"framework",
|
|
15
|
+
"compiler",
|
|
16
|
+
"jsx",
|
|
17
|
+
"babel",
|
|
18
|
+
"vite"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@babel/core": "^7.0.0"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@babel/core": "^7.23.0",
|
|
30
|
+
"@babel/preset-env": "^7.23.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Framework Babel Plugin
|
|
3
|
+
* Transforms JSX into optimized DOM operations
|
|
4
|
+
*
|
|
5
|
+
* 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
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const CONTROL_FLOW_COMPONENTS = new Set(['Show', 'For', 'Switch', 'Match', 'Suspense', 'ErrorBoundary', 'Portal']);
|
|
15
|
+
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
|
+
]);
|
|
21
|
+
|
|
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
|
+
|
|
34
|
+
// Parse event modifiers from attribute name
|
|
35
|
+
function parseEventModifiers(name) {
|
|
36
|
+
const parts = name.split('|');
|
|
37
|
+
const eventName = parts[0];
|
|
38
|
+
const modifiers = parts.slice(1).filter(m => EVENT_MODIFIERS.has(m));
|
|
39
|
+
return { eventName, modifiers };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if attribute is a binding
|
|
43
|
+
function isBindingAttribute(name) {
|
|
44
|
+
return name.startsWith('bind:');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get the binding property from bind:value -> value
|
|
48
|
+
function getBindingProperty(name) {
|
|
49
|
+
return name.slice(5); // Remove 'bind:'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check if element is a component (starts with uppercase)
|
|
53
|
+
function isComponent(name) {
|
|
54
|
+
return /^[A-Z]/.test(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
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
|
+
|
|
85
|
+
return t.stringLiteral(value.value || '');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Create event handler with modifiers
|
|
89
|
+
function createEventHandler(handler, modifiers) {
|
|
90
|
+
if (modifiers.length === 0) {
|
|
91
|
+
return handler;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build modifier chain
|
|
95
|
+
let wrappedHandler = handler;
|
|
96
|
+
|
|
97
|
+
for (const mod of modifiers) {
|
|
98
|
+
switch (mod) {
|
|
99
|
+
case 'preventDefault':
|
|
100
|
+
// (e) => { e.preventDefault(); handler(e); }
|
|
101
|
+
wrappedHandler = t.arrowFunctionExpression(
|
|
102
|
+
[t.identifier('e')],
|
|
103
|
+
t.blockStatement([
|
|
104
|
+
t.expressionStatement(
|
|
105
|
+
t.callExpression(
|
|
106
|
+
t.memberExpression(t.identifier('e'), t.identifier('preventDefault')),
|
|
107
|
+
[]
|
|
108
|
+
)
|
|
109
|
+
),
|
|
110
|
+
t.expressionStatement(
|
|
111
|
+
t.callExpression(wrappedHandler, [t.identifier('e')])
|
|
112
|
+
)
|
|
113
|
+
])
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'stopPropagation':
|
|
118
|
+
wrappedHandler = t.arrowFunctionExpression(
|
|
119
|
+
[t.identifier('e')],
|
|
120
|
+
t.blockStatement([
|
|
121
|
+
t.expressionStatement(
|
|
122
|
+
t.callExpression(
|
|
123
|
+
t.memberExpression(t.identifier('e'), t.identifier('stopPropagation')),
|
|
124
|
+
[]
|
|
125
|
+
)
|
|
126
|
+
),
|
|
127
|
+
t.expressionStatement(
|
|
128
|
+
t.callExpression(wrappedHandler, [t.identifier('e')])
|
|
129
|
+
)
|
|
130
|
+
])
|
|
131
|
+
);
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'self':
|
|
135
|
+
// (e) => { if (e.target === e.currentTarget) handler(e); }
|
|
136
|
+
wrappedHandler = t.arrowFunctionExpression(
|
|
137
|
+
[t.identifier('e')],
|
|
138
|
+
t.blockStatement([
|
|
139
|
+
t.ifStatement(
|
|
140
|
+
t.binaryExpression(
|
|
141
|
+
'===',
|
|
142
|
+
t.memberExpression(t.identifier('e'), t.identifier('target')),
|
|
143
|
+
t.memberExpression(t.identifier('e'), t.identifier('currentTarget'))
|
|
144
|
+
),
|
|
145
|
+
t.expressionStatement(
|
|
146
|
+
t.callExpression(wrappedHandler, [t.identifier('e')])
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
])
|
|
150
|
+
);
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'once':
|
|
154
|
+
case 'capture':
|
|
155
|
+
case 'passive':
|
|
156
|
+
// These are handled at addEventListener level, mark them
|
|
157
|
+
// Will be processed by the runtime
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return wrappedHandler;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Transform JSX element to createElement call
|
|
166
|
+
function transformElement(path, state) {
|
|
167
|
+
const { node } = path;
|
|
168
|
+
const openingElement = node.openingElement;
|
|
169
|
+
const tagName = openingElement.name.name;
|
|
170
|
+
const attributes = openingElement.attributes;
|
|
171
|
+
const children = node.children;
|
|
172
|
+
|
|
173
|
+
// Check if it's a component
|
|
174
|
+
if (isComponent(tagName)) {
|
|
175
|
+
return transformComponent(path, state);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Build props object
|
|
179
|
+
const props = [];
|
|
180
|
+
const eventProps = [];
|
|
181
|
+
const bindProps = [];
|
|
182
|
+
let hasSpread = false;
|
|
183
|
+
let spreadExpr = null;
|
|
184
|
+
|
|
185
|
+
for (const attr of attributes) {
|
|
186
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
187
|
+
hasSpread = true;
|
|
188
|
+
spreadExpr = attr.argument;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const attrName = attr.name.name || attr.name;
|
|
193
|
+
|
|
194
|
+
// Handle event modifiers: onClick|preventDefault
|
|
195
|
+
if (attrName.startsWith('on') && attrName.includes('|')) {
|
|
196
|
+
const { eventName, modifiers } = parseEventModifiers(attrName);
|
|
197
|
+
const handler = transformAttributeValue(attr.value);
|
|
198
|
+
const wrappedHandler = createEventHandler(handler, modifiers);
|
|
199
|
+
|
|
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
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Handle two-way binding: bind:value
|
|
216
|
+
if (isBindingAttribute(attrName)) {
|
|
217
|
+
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
|
+
);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Handle regular attributes
|
|
230
|
+
const isDynamic = t.isJSXExpressionContainer(attr.value) && mightBeSignal(attr.value?.expression);
|
|
231
|
+
const value = transformAttributeValue(attr.value, isDynamic);
|
|
232
|
+
|
|
233
|
+
// Use the correct attribute name (className -> class for DOM)
|
|
234
|
+
let domAttrName = attrName;
|
|
235
|
+
if (attrName === 'className') domAttrName = 'class';
|
|
236
|
+
if (attrName === 'htmlFor') domAttrName = 'for';
|
|
237
|
+
|
|
238
|
+
props.push(
|
|
239
|
+
t.objectProperty(
|
|
240
|
+
t.stringLiteral(domAttrName),
|
|
241
|
+
value
|
|
242
|
+
)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
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
|
+
}
|
|
305
|
+
|
|
306
|
+
if (hasSpread) {
|
|
307
|
+
// Merge spread with props
|
|
308
|
+
args.push(
|
|
309
|
+
t.callExpression(
|
|
310
|
+
t.memberExpression(t.identifier('Object'), t.identifier('assign')),
|
|
311
|
+
[t.objectExpression([]), spreadExpr, t.objectExpression(propsObj)]
|
|
312
|
+
)
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
args.push(t.objectExpression(propsObj));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Children array
|
|
319
|
+
if (transformedChildren.length > 0) {
|
|
320
|
+
args.push(t.arrayExpression(transformedChildren));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Determine if SVG context
|
|
324
|
+
const isSvg = isSVGElement(tagName);
|
|
325
|
+
|
|
326
|
+
// Call the appropriate createElement function
|
|
327
|
+
const createFn = isSvg ? '_createSVGElement' : '_createElement';
|
|
328
|
+
|
|
329
|
+
return t.callExpression(
|
|
330
|
+
t.identifier(createFn),
|
|
331
|
+
args
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Transform component JSX
|
|
336
|
+
function transformComponent(path, state) {
|
|
337
|
+
const { node } = path;
|
|
338
|
+
const openingElement = node.openingElement;
|
|
339
|
+
const componentName = openingElement.name.name;
|
|
340
|
+
const attributes = openingElement.attributes;
|
|
341
|
+
const children = node.children;
|
|
342
|
+
|
|
343
|
+
// Check for client directives (islands)
|
|
344
|
+
let clientDirective = null;
|
|
345
|
+
const filteredAttrs = [];
|
|
346
|
+
|
|
347
|
+
for (const attr of attributes) {
|
|
348
|
+
if (t.isJSXAttribute(attr)) {
|
|
349
|
+
const name = attr.name.name;
|
|
350
|
+
if (name && name.startsWith('client:')) {
|
|
351
|
+
clientDirective = name.slice(7); // 'load', 'idle', 'visible', etc.
|
|
352
|
+
if (attr.value) {
|
|
353
|
+
// client:media="(max-width: 768px)"
|
|
354
|
+
clientDirective = {
|
|
355
|
+
type: clientDirective,
|
|
356
|
+
value: attr.value.value
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
filteredAttrs.push(attr);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Build props object
|
|
366
|
+
const props = [];
|
|
367
|
+
let hasSpread = false;
|
|
368
|
+
let spreadExpr = null;
|
|
369
|
+
|
|
370
|
+
for (const attr of filteredAttrs) {
|
|
371
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
372
|
+
hasSpread = true;
|
|
373
|
+
spreadExpr = attr.argument;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const attrName = attr.name.name;
|
|
378
|
+
const value = transformAttributeValue(attr.value);
|
|
379
|
+
|
|
380
|
+
props.push(
|
|
381
|
+
t.objectProperty(
|
|
382
|
+
t.identifier(attrName),
|
|
383
|
+
value
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
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
|
+
}
|
|
423
|
+
|
|
424
|
+
// Build props expression
|
|
425
|
+
let propsExpr;
|
|
426
|
+
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 {
|
|
432
|
+
propsExpr = t.objectExpression(props);
|
|
433
|
+
}
|
|
434
|
+
|
|
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
|
+
}
|
|
442
|
+
|
|
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);
|
|
451
|
+
|
|
452
|
+
return t.callExpression(
|
|
453
|
+
t.identifier('_createIsland'),
|
|
454
|
+
[
|
|
455
|
+
t.identifier(componentName),
|
|
456
|
+
propsExpr,
|
|
457
|
+
directiveValue
|
|
458
|
+
]
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Regular component call
|
|
463
|
+
return t.callExpression(
|
|
464
|
+
t.identifier('_createComponent'),
|
|
465
|
+
[
|
|
466
|
+
t.identifier(componentName),
|
|
467
|
+
propsExpr
|
|
468
|
+
]
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
name: 'what-jsx-transform',
|
|
474
|
+
|
|
475
|
+
visitor: {
|
|
476
|
+
Program: {
|
|
477
|
+
enter(path, state) {
|
|
478
|
+
state.needsRuntime = false;
|
|
479
|
+
state.usedHelpers = new Set();
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
exit(path, state) {
|
|
483
|
+
if (state.needsRuntime) {
|
|
484
|
+
// Add runtime imports at the top
|
|
485
|
+
const helpers = Array.from(state.usedHelpers);
|
|
486
|
+
|
|
487
|
+
const importDecl = t.importDeclaration(
|
|
488
|
+
helpers.map(h => t.importSpecifier(t.identifier(h), t.identifier(h))),
|
|
489
|
+
t.stringLiteral('what-framework/runtime')
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
path.unshiftContainer('body', importDecl);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
JSXElement(path, state) {
|
|
498
|
+
state.needsRuntime = true;
|
|
499
|
+
state.usedHelpers.add('_createElement');
|
|
500
|
+
state.usedHelpers.add('_createComponent');
|
|
501
|
+
|
|
502
|
+
const transformed = transformElement(path, state);
|
|
503
|
+
path.replaceWith(transformed);
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
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
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
package/src/index.js
ADDED
package/src/runtime.js
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Framework Runtime
|
|
3
|
+
* Optimized DOM creation and updates with signal reactivity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { effect, untrack, batch } from 'what-framework';
|
|
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
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Framework Vite Plugin
|
|
3
|
+
* Enables JSX transformation and other optimizations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { transformSync } from '@babel/core';
|
|
7
|
+
import whatBabelPlugin from './babel-plugin.js';
|
|
8
|
+
|
|
9
|
+
export default function whatVitePlugin(options = {}) {
|
|
10
|
+
const {
|
|
11
|
+
// File extensions to process
|
|
12
|
+
include = /\.[jt]sx$/,
|
|
13
|
+
// Files to exclude
|
|
14
|
+
exclude = /node_modules/,
|
|
15
|
+
// Enable source maps
|
|
16
|
+
sourceMaps = true,
|
|
17
|
+
// Production optimizations
|
|
18
|
+
production = process.env.NODE_ENV === 'production'
|
|
19
|
+
} = options;
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
name: 'vite-plugin-what',
|
|
23
|
+
|
|
24
|
+
// Transform JSX files
|
|
25
|
+
transform(code, id) {
|
|
26
|
+
// Check if we should process this file
|
|
27
|
+
if (!include.test(id)) return null;
|
|
28
|
+
if (exclude && exclude.test(id)) return null;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = transformSync(code, {
|
|
32
|
+
filename: id,
|
|
33
|
+
sourceMaps,
|
|
34
|
+
plugins: [
|
|
35
|
+
[whatBabelPlugin, { production }]
|
|
36
|
+
],
|
|
37
|
+
parserOpts: {
|
|
38
|
+
plugins: ['jsx', 'typescript']
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!result || !result.code) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
code: result.code,
|
|
48
|
+
map: result.map
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[what] Error transforming ${id}:`, error.message);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Resolve what-framework imports
|
|
57
|
+
resolveId(id) {
|
|
58
|
+
if (id === 'what-framework/runtime') {
|
|
59
|
+
return '\0what-runtime';
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
// Load virtual modules
|
|
65
|
+
load(id) {
|
|
66
|
+
if (id === '\0what-runtime') {
|
|
67
|
+
// Return the runtime module
|
|
68
|
+
return `export * from 'what-compiler/runtime';`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Configure for development
|
|
74
|
+
config(config, { mode }) {
|
|
75
|
+
return {
|
|
76
|
+
esbuild: {
|
|
77
|
+
// Let our plugin handle JSX, not esbuild
|
|
78
|
+
jsx: 'preserve',
|
|
79
|
+
jsxFactory: '_createElement',
|
|
80
|
+
jsxFragment: '_Fragment'
|
|
81
|
+
},
|
|
82
|
+
optimizeDeps: {
|
|
83
|
+
// Pre-bundle the framework
|
|
84
|
+
include: ['what-framework']
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Named export for compatibility
|
|
92
|
+
export { whatVitePlugin as what };
|