@fluixi/compiler 1.0.0-alpha.53
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/LICENSE +21 -0
- package/README.md +76 -0
- package/dist/babel-OGRQKOZA.mjs +2 -0
- package/dist/chunk-3SAEGOMQ.mjs +1 -0
- package/dist/codegen/backend.cjs +1 -0
- package/dist/codegen/backend.d.ts +22 -0
- package/dist/codegen/backend.d.ts.map +1 -0
- package/dist/codegen/backend.js +1 -0
- package/dist/codegen/backend.mjs +0 -0
- package/dist/codegen/backends/imperative.cjs +1 -0
- package/dist/codegen/backends/imperative.d.ts +3 -0
- package/dist/codegen/backends/imperative.d.ts.map +1 -0
- package/dist/codegen/backends/imperative.js +152 -0
- package/dist/codegen/backends/imperative.mjs +1 -0
- package/dist/codegen/contract.cjs +1 -0
- package/dist/codegen/contract.d.ts +25 -0
- package/dist/codegen/contract.d.ts.map +1 -0
- package/dist/codegen/contract.js +30 -0
- package/dist/codegen/contract.mjs +1 -0
- package/dist/frontend/babel/build-ir.cjs +1 -0
- package/dist/frontend/babel/build-ir.d.ts +23 -0
- package/dist/frontend/babel/build-ir.d.ts.map +1 -0
- package/dist/frontend/babel/build-ir.js +224 -0
- package/dist/frontend/babel/build-ir.mjs +1 -0
- package/dist/frontend/babel/index.cjs +2 -0
- package/dist/frontend/babel/index.d.ts +34 -0
- package/dist/frontend/babel/index.d.ts.map +1 -0
- package/dist/frontend/babel/index.js +65 -0
- package/dist/frontend/babel/index.mjs +2 -0
- package/dist/frontend/babel/plugin.cjs +2 -0
- package/dist/frontend/babel/plugin.d.ts +122 -0
- package/dist/frontend/babel/plugin.d.ts.map +1 -0
- package/dist/frontend/babel/plugin.js +1509 -0
- package/dist/frontend/babel/plugin.mjs +2 -0
- package/dist/frontend/babel/server-functions.cjs +1 -0
- package/dist/frontend/babel/server-functions.d.ts +11 -0
- package/dist/frontend/babel/server-functions.d.ts.map +1 -0
- package/dist/frontend/babel/server-functions.js +88 -0
- package/dist/frontend/babel/server-functions.mjs +1 -0
- package/dist/frontend/babel/types.cjs +1 -0
- package/dist/frontend/babel/types.d.ts +34 -0
- package/dist/frontend/babel/types.d.ts.map +1 -0
- package/dist/frontend/babel/types.js +1 -0
- package/dist/frontend/babel/types.mjs +0 -0
- package/dist/frontend/types.cjs +1 -0
- package/dist/frontend/types.d.ts +26 -0
- package/dist/frontend/types.d.ts.map +1 -0
- package/dist/frontend/types.js +1 -0
- package/dist/frontend/types.mjs +0 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.mjs +1 -0
- package/dist/integrations.cjs +12 -0
- package/dist/integrations.d.ts +251 -0
- package/dist/integrations.d.ts.map +1 -0
- package/dist/integrations.js +790 -0
- package/dist/integrations.mjs +11 -0
- package/dist/ir/nodes.cjs +1 -0
- package/dist/ir/nodes.d.ts +80 -0
- package/dist/ir/nodes.d.ts.map +1 -0
- package/dist/ir/nodes.js +1 -0
- package/dist/ir/nodes.mjs +0 -0
- package/dist/options.cjs +1 -0
- package/dist/options.d.ts +18 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +9 -0
- package/dist/options.mjs +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Comprehensive Babel Plugin for Reactive JSX Transformation
|
|
3
|
+
* @module @fluixi/babel-plugin-jsx
|
|
4
|
+
*
|
|
5
|
+
* This plugin transforms JSX into optimized reactive DOM operations with:
|
|
6
|
+
* - Fine-grained reactivity detection
|
|
7
|
+
* - Signal/store wrapping
|
|
8
|
+
* - Event delegation
|
|
9
|
+
* - Static hoisting
|
|
10
|
+
* - Lit-HTML integration
|
|
11
|
+
* - Control flow optimization
|
|
12
|
+
*/
|
|
13
|
+
import { declare } from '@babel/helper-plugin-utils';
|
|
14
|
+
import { types as t, template, } from '@babel/core';
|
|
15
|
+
import { buildIR } from './build-ir.js';
|
|
16
|
+
import { imperativeBackend } from '../../codegen/backends/imperative.js';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Constants
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const CONTROL_FLOW_COMPONENTS = new Set([
|
|
21
|
+
'Show',
|
|
22
|
+
'For',
|
|
23
|
+
'Index',
|
|
24
|
+
'Switch',
|
|
25
|
+
'Match',
|
|
26
|
+
'Portal',
|
|
27
|
+
'Dynamic',
|
|
28
|
+
'ErrorBoundary',
|
|
29
|
+
'Suspense',
|
|
30
|
+
]);
|
|
31
|
+
// Control-flow components read a single reactive *value* prop inside their own tracking
|
|
32
|
+
// scope. Emit that prop as a getter (like `children`) so reading it re-runs the caller's
|
|
33
|
+
// expression — otherwise `each={items()}` / `when={cond()}` snapshot once and never update.
|
|
34
|
+
// The runtime routes these through `resolveReactiveProp`, which also unwraps the accessor
|
|
35
|
+
// form (`each={items}`), so all three shapes work with no compiler-side ambiguity.
|
|
36
|
+
//
|
|
37
|
+
// Excluded on purpose: `Dynamic.component` (a component IS a function, so the value can't be
|
|
38
|
+
// told apart from an accessor — its idiomatic `component={signal}` form already reacts) and
|
|
39
|
+
// `Portal.mount` (read once at mount, not a reactive value).
|
|
40
|
+
const CONTROL_FLOW_VALUE_PROPS = {
|
|
41
|
+
Show: 'when',
|
|
42
|
+
For: 'each',
|
|
43
|
+
Index: 'each',
|
|
44
|
+
Match: 'when',
|
|
45
|
+
};
|
|
46
|
+
const REACTIVE_PATTERNS = {
|
|
47
|
+
signalCall: /\w+\(\)$/,
|
|
48
|
+
storeAccess: /\w+\.\w+/,
|
|
49
|
+
arrowFunction: /^\(\)\s*=>/,
|
|
50
|
+
functionCall: /^function\s*\(/,
|
|
51
|
+
};
|
|
52
|
+
const EVENT_PROPS = /^on[A-Z]/;
|
|
53
|
+
const DELEGATABLE_EVENTS = [
|
|
54
|
+
'click',
|
|
55
|
+
'dblclick',
|
|
56
|
+
'input',
|
|
57
|
+
'change',
|
|
58
|
+
'submit',
|
|
59
|
+
'focus',
|
|
60
|
+
'blur',
|
|
61
|
+
'keydown',
|
|
62
|
+
'keyup',
|
|
63
|
+
'keypress',
|
|
64
|
+
'mousedown',
|
|
65
|
+
'mouseup',
|
|
66
|
+
'mouseover',
|
|
67
|
+
'mouseout',
|
|
68
|
+
'mouseenter',
|
|
69
|
+
'mouseleave',
|
|
70
|
+
];
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Main Plugin
|
|
73
|
+
// ============================================================================
|
|
74
|
+
export default declare((api, options = {}) => {
|
|
75
|
+
api.assertVersion(7);
|
|
76
|
+
const { runtime = 'automatic', importSource = '@fluixi/jsx', pragma = 'jsx', pragmaFrag = 'Fragment', development = false, detectReactivity = true, useLitHTML = true, hoistStatics = true, delegateEvents = true, delegatedEvents = DELEGATABLE_EVENTS, optimizeControlFlow = true, sourceMaps = true, signalModule = '@fluixi/dom', controlFlowModule = '@fluixi/dom', reactiveModule = '@fluixi/reactive/signal', autoShowTransform = false, backend = 'imperative', codegen = 'inline', } = options;
|
|
77
|
+
const pluginState = {
|
|
78
|
+
backend,
|
|
79
|
+
codegen,
|
|
80
|
+
irImports: new Set(),
|
|
81
|
+
libModule: '@fluixi/core',
|
|
82
|
+
hasJSX: false,
|
|
83
|
+
hasReactivity: false,
|
|
84
|
+
hasLitHTML: false,
|
|
85
|
+
needsSignalImport: false,
|
|
86
|
+
needsControlFlowImport: false,
|
|
87
|
+
needsCreateMemo: false,
|
|
88
|
+
controlFlowComponents: new Set(),
|
|
89
|
+
delegatedEvents: new Set(),
|
|
90
|
+
staticElements: new Map(),
|
|
91
|
+
staticCounter: 0,
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
name: '@fluixi/babel-plugin-jsx',
|
|
95
|
+
manipulateOptions(opts, parserOpts) {
|
|
96
|
+
// Enable JSX parsing
|
|
97
|
+
parserOpts.plugins.push('jsx', 'typescript');
|
|
98
|
+
},
|
|
99
|
+
pre(state) {
|
|
100
|
+
// Initialize plugin state
|
|
101
|
+
Object.assign(state, pluginState);
|
|
102
|
+
},
|
|
103
|
+
visitor: {
|
|
104
|
+
// Map @fluixi/core/rx imports to @fluixi/reactive for client-side code
|
|
105
|
+
ImportDeclaration(path, state) {
|
|
106
|
+
const source = path.node.source.value;
|
|
107
|
+
// Map @fluixi/core/rx to @fluixi/reactive (client-side reactive primitives)
|
|
108
|
+
if (source === '@fluixi/core/rx') {
|
|
109
|
+
path.node.source = t.stringLiteral('@fluixi/reactive');
|
|
110
|
+
}
|
|
111
|
+
// Also handle fluixi server imports - keep as is for SSR
|
|
112
|
+
// @fluixi/core/server stays as is (includes server reactive mappings)
|
|
113
|
+
},
|
|
114
|
+
Program: {
|
|
115
|
+
enter(path, state) {
|
|
116
|
+
// Reset state for new file
|
|
117
|
+
Object.assign(state, {
|
|
118
|
+
hasJSX: false,
|
|
119
|
+
hasReactivity: false,
|
|
120
|
+
hasLitHTML: false,
|
|
121
|
+
needsSignalImport: false,
|
|
122
|
+
needsLitrxImport: false,
|
|
123
|
+
needsControlFlowImport: false,
|
|
124
|
+
needsCreateMemo: false,
|
|
125
|
+
controlFlowComponents: new Set(),
|
|
126
|
+
delegatedEvents: new Set(),
|
|
127
|
+
staticElements: new Map(),
|
|
128
|
+
autoShow: autoShowTransform,
|
|
129
|
+
staticCounter: 0,
|
|
130
|
+
libModule: '@fluixi/core',
|
|
131
|
+
backend,
|
|
132
|
+
codegen,
|
|
133
|
+
irImports: new Set(),
|
|
134
|
+
needsCreateNativeElement: false,
|
|
135
|
+
needsInsert: false,
|
|
136
|
+
needsSpread: false,
|
|
137
|
+
needsCreateComponent: false,
|
|
138
|
+
needsMergeProps: false,
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
exit(path, state) {
|
|
142
|
+
const imports = [];
|
|
143
|
+
if (!state.hasJSX) {
|
|
144
|
+
// Add signal directive import if needed
|
|
145
|
+
if (state.needsSignalImport) {
|
|
146
|
+
imports.push(t.importDeclaration([
|
|
147
|
+
t.importSpecifier(t.identifier('signal'), t.identifier('signal')),
|
|
148
|
+
], t.stringLiteral(signalModule)));
|
|
149
|
+
}
|
|
150
|
+
if (state.needsLitrxImport && !path.scope.hasBinding('litrx')) {
|
|
151
|
+
imports.push(t.importDeclaration([
|
|
152
|
+
t.importSpecifier(t.identifier('litrx'), t.identifier('litrx')),
|
|
153
|
+
], t.stringLiteral(state.libModule)));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (state.hasJSX) {
|
|
157
|
+
if (backend === 'imperative' && codegen === 'ir') {
|
|
158
|
+
// IR pipeline: import exactly the symbols emit() reported, splitting
|
|
159
|
+
// control-flow components to the control-flow module.
|
|
160
|
+
const CF = new Set([
|
|
161
|
+
'Show', 'For', 'Index', 'Switch', 'Match',
|
|
162
|
+
'Dynamic', 'ErrorBoundary', 'Portal', 'Suspense',
|
|
163
|
+
]);
|
|
164
|
+
// Reactive primitives come from the signal core, not the dom module.
|
|
165
|
+
const REACTIVE = new Set([
|
|
166
|
+
'createMemo', 'createEffect', 'createRenderEffect', 'createRoot',
|
|
167
|
+
'createSignal', 'onCleanup', 'batch', 'untrack',
|
|
168
|
+
]);
|
|
169
|
+
const all = Array.from(state.irImports);
|
|
170
|
+
const spec = (n) => t.importSpecifier(t.identifier(n), t.identifier(n));
|
|
171
|
+
const unbound = (s) => !path.scope.hasBinding(s);
|
|
172
|
+
const reactive = all.filter((s) => REACTIVE.has(s) && unbound(s));
|
|
173
|
+
const core = all.filter((s) => !CF.has(s) && !REACTIVE.has(s) && unbound(s));
|
|
174
|
+
const cf = all.filter((s) => CF.has(s) && unbound(s));
|
|
175
|
+
if (reactive.length > 0) {
|
|
176
|
+
imports.push(t.importDeclaration(reactive.map(spec), t.stringLiteral(reactiveModule)));
|
|
177
|
+
}
|
|
178
|
+
if (core.length > 0) {
|
|
179
|
+
imports.push(t.importDeclaration(core.map(spec), t.stringLiteral(signalModule)));
|
|
180
|
+
}
|
|
181
|
+
if (cf.length > 0) {
|
|
182
|
+
imports.push(t.importDeclaration(cf.map(spec), t.stringLiteral(controlFlowModule)));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (backend === 'imperative') {
|
|
186
|
+
// Compiled imperative output: import only the runtime primitives
|
|
187
|
+
// actually emitted, straight from the dom runtime module.
|
|
188
|
+
const want = [
|
|
189
|
+
[!!state.needsCreateNativeElement, 'createNativeElement'],
|
|
190
|
+
[!!state.needsSpread, 'spread'],
|
|
191
|
+
[!!state.needsInsert, 'insert'],
|
|
192
|
+
[!!state.needsCreateComponent, 'createComponent'],
|
|
193
|
+
[!!state.needsMergeProps, 'mergeProps'],
|
|
194
|
+
];
|
|
195
|
+
const specifiers = want
|
|
196
|
+
.filter(([need, name]) => need && !path.scope.hasBinding(name))
|
|
197
|
+
.map(([, name]) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
198
|
+
if (specifiers.length > 0) {
|
|
199
|
+
imports.push(t.importDeclaration(specifiers, t.stringLiteral(signalModule)));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (runtime === 'automatic') {
|
|
203
|
+
// Add JSX runtime imports
|
|
204
|
+
const specifiers = [
|
|
205
|
+
t.importSpecifier(t.identifier('jsx'), t.identifier('jsx')),
|
|
206
|
+
t.importSpecifier(t.identifier('jsxs'), t.identifier('jsxs')),
|
|
207
|
+
t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment')),
|
|
208
|
+
];
|
|
209
|
+
if (development) {
|
|
210
|
+
specifiers.push(t.importSpecifier(t.identifier('jsxDEV'), t.identifier('jsxDEV')));
|
|
211
|
+
}
|
|
212
|
+
// Add mergeProps import if spreads are used
|
|
213
|
+
if (state.needsMergeProps) {
|
|
214
|
+
specifiers.push(t.importSpecifier(t.identifier('mergeProps'), t.identifier('mergeProps')));
|
|
215
|
+
}
|
|
216
|
+
imports.push(t.importDeclaration(specifiers, t.stringLiteral(importSource)));
|
|
217
|
+
}
|
|
218
|
+
// createMemo (wraps conditionals/components) is a reactive primitive,
|
|
219
|
+
// so it comes from the reactive module, not dom.
|
|
220
|
+
if (state.needsCreateMemo && !path.scope.hasBinding('createMemo')) {
|
|
221
|
+
imports.push(t.importDeclaration([t.importSpecifier(t.identifier('createMemo'), t.identifier('createMemo'))], t.stringLiteral(backend === 'imperative'
|
|
222
|
+
? reactiveModule
|
|
223
|
+
: state.libModule ?? '@fluixi/core')));
|
|
224
|
+
}
|
|
225
|
+
// Add control flow imports for auto-transformed components (Show, etc.)
|
|
226
|
+
// Only add components that aren't already bound in the file scope.
|
|
227
|
+
if (state.needsControlFlowImport &&
|
|
228
|
+
state.controlFlowComponents.size > 0) {
|
|
229
|
+
const unbound = Array.from(state.controlFlowComponents).filter((name) => !path.scope.hasBinding(name));
|
|
230
|
+
if (unbound.length > 0) {
|
|
231
|
+
const specifiers = unbound.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
232
|
+
imports.push(t.importDeclaration(specifiers, t.stringLiteral(controlFlowModule)));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Add event delegation setup if needed
|
|
236
|
+
if (delegateEvents && state.delegatedEvents.size > 0) {
|
|
237
|
+
imports.push(t.importDeclaration([
|
|
238
|
+
t.importSpecifier(t.identifier('delegateEvents'), t.identifier('delegateEvents')),
|
|
239
|
+
], t.stringLiteral(backend === 'imperative' ? signalModule : '@fluixi/jsx')));
|
|
240
|
+
// Add delegateEvents call at top of program
|
|
241
|
+
const eventsArray = t.arrayExpression(Array.from(state.delegatedEvents).map((event) => t.stringLiteral(event)));
|
|
242
|
+
const delegateCall = t.expressionStatement(t.callExpression(t.identifier('delegateEvents'), [eventsArray]));
|
|
243
|
+
path.node.body.unshift(delegateCall);
|
|
244
|
+
}
|
|
245
|
+
// Add hoisted static elements
|
|
246
|
+
if (hoistStatics && state.staticElements.size > 0) {
|
|
247
|
+
const hoisted = [];
|
|
248
|
+
state.staticElements.forEach((value, key) => {
|
|
249
|
+
hoisted.push(t.variableDeclaration('const', [
|
|
250
|
+
t.variableDeclarator(t.identifier(key), value),
|
|
251
|
+
]));
|
|
252
|
+
});
|
|
253
|
+
path.node.body.unshift(...hoisted);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Add all imports at the top
|
|
257
|
+
if (imports.length > 0) {
|
|
258
|
+
// console.log('imports are', imports);
|
|
259
|
+
// console.log('imports', imports);
|
|
260
|
+
path.node.body.unshift(...imports);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
JSXElement(path, state) {
|
|
265
|
+
state.hasJSX = true;
|
|
266
|
+
// IR pipeline path: build IR -> emit() -> reparse. Handles the whole
|
|
267
|
+
// subtree, so we bypass the inline transform entirely.
|
|
268
|
+
if (backend === 'imperative' && codegen === 'ir') {
|
|
269
|
+
path.replaceWith(emitViaIR(path.node, state));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const element = path.node;
|
|
273
|
+
const openingElement = element.openingElement;
|
|
274
|
+
// Check if it's a control flow component
|
|
275
|
+
if (t.isJSXIdentifier(openingElement.name) &&
|
|
276
|
+
CONTROL_FLOW_COMPONENTS.has(openingElement.name.name)) {
|
|
277
|
+
state.controlFlowComponents.add(openingElement.name.name);
|
|
278
|
+
if (optimizeControlFlow) {
|
|
279
|
+
const optimized = optimizeControlFlowComponent(element, state);
|
|
280
|
+
if (optimized) {
|
|
281
|
+
path.replaceWith(optimized);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Try to hoist static elements.
|
|
287
|
+
// Not in imperative mode: hoisting a live DOM node would share one node
|
|
288
|
+
// across renders. Static hoisting there needs template-clone (contract v2).
|
|
289
|
+
if (hoistStatics && backend !== 'imperative' && isStaticElement(element, state)) {
|
|
290
|
+
const hoisted = hoistStaticElement(element, state);
|
|
291
|
+
if (hoisted) {
|
|
292
|
+
path.replaceWith(hoisted);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Transform to JSX runtime calls
|
|
297
|
+
const transformed = transformJSXElement(element, state, {
|
|
298
|
+
runtime,
|
|
299
|
+
pragma,
|
|
300
|
+
development,
|
|
301
|
+
detectReactivity,
|
|
302
|
+
useLitHTML,
|
|
303
|
+
autoShow: autoShowTransform,
|
|
304
|
+
});
|
|
305
|
+
path.replaceWith(transformed);
|
|
306
|
+
},
|
|
307
|
+
JSXFragment(path, state) {
|
|
308
|
+
state.hasJSX = true;
|
|
309
|
+
if (backend === 'imperative' && codegen === 'ir') {
|
|
310
|
+
path.replaceWith(emitViaIR(path.node, state));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const transformed = transformJSXFragment(path.node, state, {
|
|
314
|
+
runtime,
|
|
315
|
+
pragmaFrag,
|
|
316
|
+
development,
|
|
317
|
+
});
|
|
318
|
+
path.replaceWith(transformed);
|
|
319
|
+
},
|
|
320
|
+
// Detect reactive patterns in code
|
|
321
|
+
CallExpression(path, state) {
|
|
322
|
+
if (!detectReactivity)
|
|
323
|
+
return;
|
|
324
|
+
const callee = path.node.callee;
|
|
325
|
+
if (callee.name === 'handleToggle') {
|
|
326
|
+
// console.log('call express', path);
|
|
327
|
+
}
|
|
328
|
+
// Detect signal creation
|
|
329
|
+
if (t.isIdentifier(callee) &&
|
|
330
|
+
(callee.name === 'createSignal' ||
|
|
331
|
+
callee.name === 'createStore' ||
|
|
332
|
+
callee.name === 'useContext' ||
|
|
333
|
+
callee.name === 'useLocation')) {
|
|
334
|
+
// console.log('call express', callee.name);
|
|
335
|
+
state.hasReactivity = true;
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
// NOTE: JSXExpressionContainer visitor is intentionally a no-op.
|
|
339
|
+
// JSXElement.enter fires BEFORE children are traversed, so by the time
|
|
340
|
+
// Babel would visit nested JSXExpressionContainers the JSXElement has
|
|
341
|
+
// already been replaced via path.replaceWith(). Attribute and children
|
|
342
|
+
// expressions are processed directly in transformAttributeValue /
|
|
343
|
+
// transformChildren above.
|
|
344
|
+
JSXExpressionContainer(_path) {
|
|
345
|
+
// no-op
|
|
346
|
+
},
|
|
347
|
+
TaggedTemplateExpression(path, state) {
|
|
348
|
+
// const expressions = t.isTaggedTemplateExpression(path.node)
|
|
349
|
+
// ? (path as NodePath<t.TemplateLiteral>).get('expressions')
|
|
350
|
+
// : [(path as NodePath<t.JSXExpressionContainer>).get('expression')];
|
|
351
|
+
// if (t.isJSXExpressionContainer(path.node)) {
|
|
352
|
+
// console.log('is jsx');
|
|
353
|
+
// }
|
|
354
|
+
const { tag, quasi } = path.node;
|
|
355
|
+
// 1. Only target html`...`
|
|
356
|
+
// if (!t.isIdentifier(tag) || tag.name !== 'html') return;
|
|
357
|
+
path
|
|
358
|
+
.get('quasi')
|
|
359
|
+
.get('expressions')
|
|
360
|
+
.forEach((expPath, index) => {
|
|
361
|
+
const node = expPath.node;
|
|
362
|
+
// 2. Attribute Check: Look at the string part right before the expression
|
|
363
|
+
// Lit attributes look like: name="${exp}" or @click="${exp}"
|
|
364
|
+
const prevStringPart = quasi.quasis[index].value.raw;
|
|
365
|
+
const isAttribute = prevStringPart.trim().endsWith('=') ||
|
|
366
|
+
prevStringPart.match(/@[\w-]+$/);
|
|
367
|
+
if (isAttribute)
|
|
368
|
+
return; // Skip wrapping event listeners or attributes
|
|
369
|
+
// 3. Narrowing for Signal Calls: count()
|
|
370
|
+
// const isSignalCall = t.isCallExpression(node);
|
|
371
|
+
const isSignalCall = false;
|
|
372
|
+
// 4. Narrowing for Member Access: item.$name
|
|
373
|
+
let isReactiveMember = false;
|
|
374
|
+
if (t.isMemberExpression(node)) {
|
|
375
|
+
const property = node.property;
|
|
376
|
+
// Use Type Guard to narrow from Expression | PrivateName to Identifier
|
|
377
|
+
if (t.isIdentifier(property)) {
|
|
378
|
+
// console.log('exp', node);
|
|
379
|
+
property.name = property.name.replace('$', '');
|
|
380
|
+
state.needsLitrxImport = true;
|
|
381
|
+
isReactiveMember = true;
|
|
382
|
+
// if (property.name.startsWith('$')) {
|
|
383
|
+
// }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (isSignalCall || isReactiveMember) {
|
|
387
|
+
// Transform: ${exp} -> ${watch(() => exp)}
|
|
388
|
+
expPath.replaceWith(
|
|
389
|
+
//t.identifier('litrx')
|
|
390
|
+
// t.identifier('(window as any).Fluixi.litrx')
|
|
391
|
+
t.callExpression(t.identifier('((window as any).Fluixi.litrx || litrx)'), [t.arrowFunctionExpression([], node)]));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
// 'JSXExpressionContainer|TaggedTemplateExpression'(path, state) {
|
|
396
|
+
// const expressions = t.isTaggedTemplateExpression(path.node)
|
|
397
|
+
// ? (path as NodePath<t.TemplateLiteral>).get('expressions')
|
|
398
|
+
// : [(path as NodePath<t.JSXExpressionContainer>).get('expression')];
|
|
399
|
+
// if (t.isJSXExpressionContainer(path.node)) {
|
|
400
|
+
// console.log('is jsx');
|
|
401
|
+
// }
|
|
402
|
+
// console.log('exp', expressions);
|
|
403
|
+
// // const { tag, quasi } = path.node;
|
|
404
|
+
// // 1. Only target html`...`
|
|
405
|
+
// // if (!t.isIdentifier(tag) || tag.name !== 'html') return;
|
|
406
|
+
// // path
|
|
407
|
+
// // .get('quasi')
|
|
408
|
+
// // .get('expressions')
|
|
409
|
+
// // .forEach((expPath, index) => {
|
|
410
|
+
// // const node = expPath.node;
|
|
411
|
+
// // // 2. Attribute Check: Look at the string part right before the expression
|
|
412
|
+
// // // Lit attributes look like: name="${exp}" or @click="${exp}"
|
|
413
|
+
// // const prevStringPart = quasi.quasis[index].value.raw;
|
|
414
|
+
// // const isAttribute =
|
|
415
|
+
// // prevStringPart.trim().endsWith('=') ||
|
|
416
|
+
// // prevStringPart.match(/@[\w-]+$/);
|
|
417
|
+
// // if (isAttribute) return; // Skip wrapping event listeners or attributes
|
|
418
|
+
// // // 3. Narrowing for Signal Calls: count()
|
|
419
|
+
// // // const isSignalCall = t.isCallExpression(node);
|
|
420
|
+
// // const isSignalCall = false;
|
|
421
|
+
// // // 4. Narrowing for Member Access: item.$name
|
|
422
|
+
// // let isReactiveMember = false;
|
|
423
|
+
// // if (t.isMemberExpression(node)) {
|
|
424
|
+
// // const property = node.property;
|
|
425
|
+
// // // Use Type Guard to narrow from Expression | PrivateName to Identifier
|
|
426
|
+
// // if (t.isIdentifier(property)) {
|
|
427
|
+
// // if (property.name.startsWith('$')) {
|
|
428
|
+
// // (property as any).name = property.name.replace('$', '');
|
|
429
|
+
// // console.log('node', node);
|
|
430
|
+
// // state.needsLitrxImport = true;
|
|
431
|
+
// // isReactiveMember = true;
|
|
432
|
+
// // }
|
|
433
|
+
// // }
|
|
434
|
+
// // }
|
|
435
|
+
// // if (isSignalCall || isReactiveMember) {
|
|
436
|
+
// // // Transform: ${exp} -> ${watch(() => exp)}
|
|
437
|
+
// // expPath.replaceWith(
|
|
438
|
+
// // t.callExpression(t.identifier('litrx'), [
|
|
439
|
+
// // t.arrowFunctionExpression([], node as t.Expression),
|
|
440
|
+
// // ])
|
|
441
|
+
// // );
|
|
442
|
+
// // }
|
|
443
|
+
// // });
|
|
444
|
+
// // path
|
|
445
|
+
// // .get('quasi')
|
|
446
|
+
// // .get('expressions')
|
|
447
|
+
// if (t.isTaggedTemplateExpression(path.node)) {
|
|
448
|
+
// expressions.forEach((expPath, index) => {
|
|
449
|
+
// const node = expPath.node;
|
|
450
|
+
// if (!node || t.isJSXEmptyExpression(node)) return;
|
|
451
|
+
// // 1. Skip Attributes (@click, id=, etc)
|
|
452
|
+
// if (t.isTaggedTemplateExpression(path.node)) {
|
|
453
|
+
// const { tag, quasi } = path.node;
|
|
454
|
+
// const prevString = quasi.quasis[index].value.raw;
|
|
455
|
+
// if (
|
|
456
|
+
// prevString.trim().endsWith('=') ||
|
|
457
|
+
// prevString.match(/@[\w-]+$/)
|
|
458
|
+
// ) {
|
|
459
|
+
// return;
|
|
460
|
+
// }
|
|
461
|
+
// }
|
|
462
|
+
// let shouldWrap = false;
|
|
463
|
+
// let newNode: t.Expression = node as t.Expression;
|
|
464
|
+
// let property: t.Expression;
|
|
465
|
+
// // 2. Handle Member Expressions (e.g., user.$name)
|
|
466
|
+
// if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
|
467
|
+
// const propName = node.property.name;
|
|
468
|
+
// property = node.property;
|
|
469
|
+
// // Check where the object (e.g., 'user') came from
|
|
470
|
+
// const objectName = t.isIdentifier(node.object)
|
|
471
|
+
// ? node.object.name
|
|
472
|
+
// : null;
|
|
473
|
+
// const binding = objectName
|
|
474
|
+
// ? expPath.scope.getBinding(objectName)
|
|
475
|
+
// : null;
|
|
476
|
+
// // Identify if the source is one of our reactive hooks
|
|
477
|
+
// const isReactiveSource =
|
|
478
|
+
// binding &&
|
|
479
|
+
// t.isVariableDeclarator(binding.path.node) &&
|
|
480
|
+
// t.isCallExpression(binding.path.node.init) &&
|
|
481
|
+
// t.isIdentifier(binding.path.node.init.callee) &&
|
|
482
|
+
// ['useReducer', 'createStore', 'useContext'].includes(
|
|
483
|
+
// binding.path.node.init.callee.name
|
|
484
|
+
// );
|
|
485
|
+
// if (isReactiveSource || propName.startsWith('$')) {
|
|
486
|
+
// shouldWrap = true;
|
|
487
|
+
// state.needsLitrxImport = true;
|
|
488
|
+
// console.log('nnedd wrap');
|
|
489
|
+
// // TRANSFORM: Strip the '$' for the runtime Proxy access
|
|
490
|
+
// // user.$name -> user.name
|
|
491
|
+
// }
|
|
492
|
+
// if (propName.startsWith('$')) {
|
|
493
|
+
// if (property) {
|
|
494
|
+
// (property as any).name = propName.replace('$', '');
|
|
495
|
+
// newNode = t.memberExpression(node.object, property);
|
|
496
|
+
// }
|
|
497
|
+
// }
|
|
498
|
+
// }
|
|
499
|
+
// // // 3. Handle Signal Calls: count()
|
|
500
|
+
// // if (t.isCallExpression(node)) {
|
|
501
|
+
// // shouldWrap = true;
|
|
502
|
+
// // }
|
|
503
|
+
// if (shouldWrap) {
|
|
504
|
+
// state.needsLitrxImport = true;
|
|
505
|
+
// expPath.replaceWith(
|
|
506
|
+
// t.callExpression(t.identifier('litrx'), [
|
|
507
|
+
// t.arrowFunctionExpression([], newNode),
|
|
508
|
+
// ])
|
|
509
|
+
// );
|
|
510
|
+
// }
|
|
511
|
+
// });
|
|
512
|
+
// } else {
|
|
513
|
+
// console.log('jsx', expressions);
|
|
514
|
+
// }
|
|
515
|
+
// },
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
});
|
|
519
|
+
function needReativeTransform(node, expPath) {
|
|
520
|
+
if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
|
521
|
+
// const propName = node.property.name;
|
|
522
|
+
// property = node.property;
|
|
523
|
+
// Check where the object (e.g., 'user') came from
|
|
524
|
+
const objectName = t.isIdentifier(node.object) ? node.object.name : null;
|
|
525
|
+
const binding = objectName ? expPath.scope.getBinding(objectName) : null;
|
|
526
|
+
// Identify if the source is one of our reactive hooks
|
|
527
|
+
const isReactiveSource = binding &&
|
|
528
|
+
t.isVariableDeclarator(binding.path.node) &&
|
|
529
|
+
t.isCallExpression(binding.path.node.init) &&
|
|
530
|
+
t.isIdentifier(binding.path.node.init.callee) &&
|
|
531
|
+
['useReducer', 'createStore', 'useContext', 'useLocation'].includes(binding.path.node.init.callee.name);
|
|
532
|
+
return isReactiveSource || false;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// JSX Transformation
|
|
538
|
+
// ============================================================================
|
|
539
|
+
function transformJSXElement(element, state, options) {
|
|
540
|
+
const { runtime, pragma, development, autoShow = true } = options;
|
|
541
|
+
if (runtime === 'automatic') {
|
|
542
|
+
return transformAutomaticElement(element, state, development, autoShow);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
return transformClassicElement(element, state, pragma);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// Imperative backend emission
|
|
550
|
+
//
|
|
551
|
+
// Emits direct calls into the @fluixi/dom runtime instead of jsx()/jsxs():
|
|
552
|
+
// native element -> (() => { const _el = createNativeElement(tag);
|
|
553
|
+
// spread({ element: _el, props });
|
|
554
|
+
// insert(_el, children);
|
|
555
|
+
// return _el; })()
|
|
556
|
+
// component -> createMemo(() => createComponent(Comp, props))
|
|
557
|
+
// This matches @fluixi/jsx's component semantics exactly while removing
|
|
558
|
+
// the per-render jsx() dispatch for host elements. Targets RUNTIME_CONTRACT_V1.
|
|
559
|
+
// ============================================================================
|
|
560
|
+
// Build-time SVG detection, kept in lockstep with isSVGElement in the dom runtime.
|
|
561
|
+
const SVG_TAGS = new Set([
|
|
562
|
+
'svg', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline',
|
|
563
|
+
'ellipse', 'g', 'defs', 'clipPath', 'text',
|
|
564
|
+
]);
|
|
565
|
+
/** Build the props object, merging spreads via mergeProps (reactivity-preserving). */
|
|
566
|
+
function buildPropsObject(props, spreads, state) {
|
|
567
|
+
if (spreads.length > 0) {
|
|
568
|
+
state.needsMergeProps = true;
|
|
569
|
+
return t.callExpression(t.identifier('mergeProps'), [
|
|
570
|
+
...spreads,
|
|
571
|
+
t.objectExpression(props),
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
574
|
+
return t.objectExpression(props);
|
|
575
|
+
}
|
|
576
|
+
/** Wrap children expressions into a single value for `insert` / `children` prop. */
|
|
577
|
+
function childrenValue(children) {
|
|
578
|
+
return children.length === 1 ? children[0] : t.arrayExpression(children);
|
|
579
|
+
}
|
|
580
|
+
function emitImperativeElement(tag, isNative, props, spreads, children, state) {
|
|
581
|
+
if (!isNative) {
|
|
582
|
+
// Component: children go into props as a LAZY getter, so child components are
|
|
583
|
+
// created inside the component's execution (owner nesting for context/Suspense),
|
|
584
|
+
// after the component runs (provider ordering), and lazily (ErrorBoundary catch).
|
|
585
|
+
const allProps = [...props];
|
|
586
|
+
if (children.length > 0) {
|
|
587
|
+
allProps.push(t.objectMethod('get', t.identifier('children'), [], t.blockStatement([t.returnStatement(childrenValue(children))])));
|
|
588
|
+
}
|
|
589
|
+
return emitComponentCall(tag, buildPropsObject(allProps, spreads, state), state);
|
|
590
|
+
}
|
|
591
|
+
// Native element: createNativeElement + spread (attrs only) + insert (children).
|
|
592
|
+
const tagName = tag.value;
|
|
593
|
+
const isSVG = SVG_TAGS.has(tagName);
|
|
594
|
+
const elId = t.identifier('_el$');
|
|
595
|
+
const stmts = [];
|
|
596
|
+
state.needsCreateNativeElement = true;
|
|
597
|
+
const createArgs = [tag];
|
|
598
|
+
if (isSVG)
|
|
599
|
+
createArgs.push(t.booleanLiteral(true));
|
|
600
|
+
stmts.push(t.variableDeclaration('const', [
|
|
601
|
+
t.variableDeclarator(elId, t.callExpression(t.identifier('createNativeElement'), createArgs)),
|
|
602
|
+
]));
|
|
603
|
+
if (props.length > 0 || spreads.length > 0) {
|
|
604
|
+
state.needsSpread = true;
|
|
605
|
+
const spreadProps = [
|
|
606
|
+
t.objectProperty(t.identifier('element'), elId),
|
|
607
|
+
t.objectProperty(t.identifier('props'), buildPropsObject(props, spreads, state)),
|
|
608
|
+
];
|
|
609
|
+
if (isSVG) {
|
|
610
|
+
spreadProps.push(t.objectProperty(t.identifier('isSVG'), t.booleanLiteral(true)));
|
|
611
|
+
}
|
|
612
|
+
stmts.push(t.expressionStatement(t.callExpression(t.identifier('spread'), [t.objectExpression(spreadProps)])));
|
|
613
|
+
}
|
|
614
|
+
// Insert each child individually (like the jsx runtime's applyProps), so a
|
|
615
|
+
// reactive/component child that isn't the only child still gets its own
|
|
616
|
+
// reactive binding. A single insert(el, [array]) does NOT bind function
|
|
617
|
+
// elements sitting inside the array.
|
|
618
|
+
if (children.length > 0) {
|
|
619
|
+
state.needsInsert = true;
|
|
620
|
+
for (const child of children) {
|
|
621
|
+
const isFn = t.isArrowFunctionExpression(child) ||
|
|
622
|
+
t.isFunctionExpression(child) ||
|
|
623
|
+
(t.isCallExpression(child) &&
|
|
624
|
+
t.isIdentifier(child.callee) &&
|
|
625
|
+
child.callee.name === 'createMemo');
|
|
626
|
+
const args = isFn
|
|
627
|
+
? [t.cloneNode(elId), child, t.nullLiteral()]
|
|
628
|
+
: [t.cloneNode(elId), child];
|
|
629
|
+
stmts.push(t.expressionStatement(t.callExpression(t.identifier('insert'), args)));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
stmts.push(t.returnStatement(elId));
|
|
633
|
+
return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(stmts)), []);
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Emit a component invocation. jsx backend: `jsx(Comp, props)`. Imperative:
|
|
637
|
+
* `createMemo(() => createComponent(Comp, props))` — same shape jsx-runtime
|
|
638
|
+
* produces for components, so it stays reactive when used as a child.
|
|
639
|
+
*/
|
|
640
|
+
function emitComponentCall(comp, propsObj, state) {
|
|
641
|
+
if (state.backend === 'imperative') {
|
|
642
|
+
state.needsCreateComponent = true;
|
|
643
|
+
state.needsCreateMemo = true;
|
|
644
|
+
return t.callExpression(t.identifier('createMemo'), [
|
|
645
|
+
t.arrowFunctionExpression([], t.callExpression(t.identifier('createComponent'), [comp, propsObj])),
|
|
646
|
+
]);
|
|
647
|
+
}
|
|
648
|
+
return t.callExpression(t.identifier('jsx'), [comp, propsObj]);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* IR-pipeline path (codegen: 'ir'): build host-agnostic IR from the JSX node, run
|
|
652
|
+
* the imperative backend's emit(), and reparse the emitted code into an AST
|
|
653
|
+
* expression. Records the runtime symbols used so Program.exit can import them.
|
|
654
|
+
* Produces the same runtime calls as the inline path — this is the route a future
|
|
655
|
+
* swc front-end reuses.
|
|
656
|
+
*/
|
|
657
|
+
function emitViaIR(node, state) {
|
|
658
|
+
// `used` collects runtime symbols emitted for JSX nested inside expressions
|
|
659
|
+
// (render-props, ternaries, fallback={<p/>}, …) so we import them too.
|
|
660
|
+
const used = new Set();
|
|
661
|
+
const ir = buildIR(node, used);
|
|
662
|
+
const { code, imports } = imperativeBackend.emit(ir, {});
|
|
663
|
+
for (const s of imports)
|
|
664
|
+
used.add(s);
|
|
665
|
+
for (const s of used)
|
|
666
|
+
state.irImports.add(s);
|
|
667
|
+
return template.expression(code, { placeholderPattern: false })();
|
|
668
|
+
}
|
|
669
|
+
//This method will convert props to lazy props like
|
|
670
|
+
function lazyProps(properties) {
|
|
671
|
+
return properties.map((prop) => {
|
|
672
|
+
// Only transform normal properties (e.g., { open: variable })
|
|
673
|
+
// Skip methods, spread elements, or already existing getters
|
|
674
|
+
if (t.isObjectProperty(prop)) {
|
|
675
|
+
// Handle potential computed properties (like {[someKey]: variable})
|
|
676
|
+
const key = prop.key;
|
|
677
|
+
const computed = prop.computed;
|
|
678
|
+
const value = prop.value;
|
|
679
|
+
// Create a getter method: get key() { return value; }
|
|
680
|
+
return t.objectMethod('get', // Kind of method
|
|
681
|
+
key, // Property key
|
|
682
|
+
[], // Getters take no arguments
|
|
683
|
+
t.blockStatement([
|
|
684
|
+
// Body: { return value; }
|
|
685
|
+
t.returnStatement(value),
|
|
686
|
+
]), computed // Preserve computed status if true
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
// Keep existing ObjectMethods (like get already there, or regular methods) as-is
|
|
690
|
+
return prop;
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
;
|
|
694
|
+
function transformAutomaticElement(element, state, development, autoShow = true) {
|
|
695
|
+
const openingElement = element.openingElement;
|
|
696
|
+
const children = element.children;
|
|
697
|
+
// Get element name/tag
|
|
698
|
+
const tag = getElementName(openingElement.name);
|
|
699
|
+
// Only auto-wrap reactive expressions for native HTML elements (lowercase tag).
|
|
700
|
+
// Component props must NOT be wrapped — the component receives the value
|
|
701
|
+
// as-is and is responsible for its own reactivity.
|
|
702
|
+
const isNativeElement = t.isJSXIdentifier(openingElement.name) &&
|
|
703
|
+
/^[a-z]/.test(openingElement.name.name);
|
|
704
|
+
// Transform attributes to props object
|
|
705
|
+
const attrs = transformAttributes(openingElement.attributes, state, isNativeElement, t.isJSXIdentifier(openingElement.name) ? openingElement.name.name : '');
|
|
706
|
+
let props = attrs.props;
|
|
707
|
+
const spreads = attrs.spreads;
|
|
708
|
+
// Component props become lazy getters so the component is created ONCE and reactivity flows
|
|
709
|
+
// through the getter (a reactive `prop={sig()}` read eagerly would re-create the whole
|
|
710
|
+
// component via the enclosing createMemo, so a <Portal> re-mounts and piles up). Static props
|
|
711
|
+
// as getters are harmless. Native-element attrs must NOT be wrapped: they already carry their
|
|
712
|
+
// own reactivity as `() => expr` thunks that `spread` consumes.
|
|
713
|
+
if (!isNativeElement)
|
|
714
|
+
props = lazyProps(props);
|
|
715
|
+
// Transform children
|
|
716
|
+
const transformedChildren = transformChildren(children, state, autoShow);
|
|
717
|
+
// Imperative backend: emit direct runtime calls instead of jsx()/jsxs().
|
|
718
|
+
if (state.backend === 'imperative') {
|
|
719
|
+
return emitImperativeElement(tag, isNativeElement, props, spreads, transformedChildren, state);
|
|
720
|
+
}
|
|
721
|
+
// Add children to props if any
|
|
722
|
+
if (transformedChildren.length > 0) {
|
|
723
|
+
const childrenProp = transformedChildren.length === 1
|
|
724
|
+
? transformedChildren[0]
|
|
725
|
+
: t.arrayExpression(transformedChildren);
|
|
726
|
+
props.push(t.objectProperty(t.identifier('children'), childrenProp));
|
|
727
|
+
}
|
|
728
|
+
// Build props object with spreads
|
|
729
|
+
let propsObj;
|
|
730
|
+
if (spreads.length > 0) {
|
|
731
|
+
// Use mergeProps helper to preserve reactivity for store proxies
|
|
732
|
+
const baseProps = props.length > 0 ? t.objectExpression(props) : t.objectExpression([]);
|
|
733
|
+
// Import mergeProps from jsx-runtime if spreads are used
|
|
734
|
+
state.needsMergeProps = true;
|
|
735
|
+
propsObj = t.callExpression(t.identifier('mergeProps'), [
|
|
736
|
+
...spreads,
|
|
737
|
+
baseProps,
|
|
738
|
+
]);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
propsObj = t.objectExpression(props);
|
|
742
|
+
}
|
|
743
|
+
// Determine function name
|
|
744
|
+
const functionName = development
|
|
745
|
+
? 'jsxDEV'
|
|
746
|
+
: transformedChildren.length > 1
|
|
747
|
+
? 'jsxs'
|
|
748
|
+
: 'jsx';
|
|
749
|
+
// Build jsx call
|
|
750
|
+
const args = [tag, propsObj];
|
|
751
|
+
if (development) {
|
|
752
|
+
// Add key, isStaticChildren, source, self for dev mode
|
|
753
|
+
args.push(t.identifier('undefined'), // key
|
|
754
|
+
t.booleanLiteral(false), // isStaticChildren
|
|
755
|
+
t.identifier('undefined'), // source
|
|
756
|
+
t.identifier('undefined') // self
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
return t.callExpression(t.identifier(functionName), args);
|
|
760
|
+
}
|
|
761
|
+
function transformClassicElement(element, state, pragma) {
|
|
762
|
+
const openingElement = element.openingElement;
|
|
763
|
+
const children = element.children;
|
|
764
|
+
const tag = getElementName(openingElement.name);
|
|
765
|
+
const isNativeElement = t.isJSXIdentifier(openingElement.name) &&
|
|
766
|
+
/^[a-z]/.test(openingElement.name.name);
|
|
767
|
+
const { props, spreads } = transformAttributes(openingElement.attributes, state, isNativeElement, t.isJSXIdentifier(openingElement.name) ? openingElement.name.name : '');
|
|
768
|
+
const transformedChildren = transformChildren(children, state, state.autoShow ?? true);
|
|
769
|
+
let propsObj;
|
|
770
|
+
if (spreads.length > 0) {
|
|
771
|
+
// Use mergeProps helper to preserve reactivity for store proxies
|
|
772
|
+
// mergeProps(...spreads, baseProps) instead of Object.assign
|
|
773
|
+
const baseProps = props.length > 0 ? t.objectExpression(props) : t.objectExpression([]);
|
|
774
|
+
// Import mergeProps from jsx-runtime if spreads are used
|
|
775
|
+
state.needsMergeProps = true;
|
|
776
|
+
propsObj = t.callExpression(t.identifier('mergeProps'), [
|
|
777
|
+
...spreads,
|
|
778
|
+
baseProps,
|
|
779
|
+
]);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
propsObj = props.length > 0 ? t.objectExpression(props) : t.nullLiteral();
|
|
783
|
+
}
|
|
784
|
+
const args = [tag, propsObj, ...transformedChildren];
|
|
785
|
+
return t.callExpression(t.identifier(pragma), args);
|
|
786
|
+
}
|
|
787
|
+
function transformJSXFragment(fragment, state, options) {
|
|
788
|
+
const { runtime, pragmaFrag, development } = options;
|
|
789
|
+
const children = transformChildren(fragment.children, state, state.autoShow ?? true);
|
|
790
|
+
// Imperative backend: a fragment is just its children; insert() flattens arrays.
|
|
791
|
+
if (state.backend === 'imperative') {
|
|
792
|
+
if (children.length === 0)
|
|
793
|
+
return t.nullLiteral();
|
|
794
|
+
return childrenValue(children);
|
|
795
|
+
}
|
|
796
|
+
if (runtime === 'automatic') {
|
|
797
|
+
const functionName = development
|
|
798
|
+
? 'jsxDEV'
|
|
799
|
+
: children.length > 1
|
|
800
|
+
? 'jsxs'
|
|
801
|
+
: 'jsx';
|
|
802
|
+
const props = t.objectExpression([
|
|
803
|
+
t.objectProperty(t.identifier('children'), children.length === 1 ? children[0] : t.arrayExpression(children)),
|
|
804
|
+
]);
|
|
805
|
+
const args = [t.identifier('Fragment'), props];
|
|
806
|
+
if (development) {
|
|
807
|
+
args.push(t.identifier('undefined'), t.booleanLiteral(false), t.identifier('undefined'), t.identifier('undefined'));
|
|
808
|
+
}
|
|
809
|
+
return t.callExpression(t.identifier(functionName), args);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
return t.callExpression(t.identifier(pragmaFrag), children);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// ============================================================================
|
|
816
|
+
// Element Name Transformation
|
|
817
|
+
// ============================================================================
|
|
818
|
+
function getElementName(name) {
|
|
819
|
+
if (t.isJSXIdentifier(name)) {
|
|
820
|
+
const nameStr = name.name;
|
|
821
|
+
// Check if it's a native element (lowercase)
|
|
822
|
+
if (nameStr[0] === nameStr[0].toLowerCase()) {
|
|
823
|
+
return t.stringLiteral(nameStr);
|
|
824
|
+
}
|
|
825
|
+
// Component reference
|
|
826
|
+
return t.identifier(nameStr);
|
|
827
|
+
}
|
|
828
|
+
if (t.isJSXMemberExpression(name)) {
|
|
829
|
+
return transformJSXMemberExpression(name);
|
|
830
|
+
}
|
|
831
|
+
if (t.isJSXNamespacedName(name)) {
|
|
832
|
+
return t.stringLiteral(`${name.namespace.name}:${name.name.name}`);
|
|
833
|
+
}
|
|
834
|
+
return t.stringLiteral('div');
|
|
835
|
+
}
|
|
836
|
+
function transformJSXMemberExpression(expr) {
|
|
837
|
+
let object;
|
|
838
|
+
if (t.isJSXIdentifier(expr.object)) {
|
|
839
|
+
object = t.identifier(expr.object.name);
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
object = transformJSXMemberExpression(expr.object);
|
|
843
|
+
}
|
|
844
|
+
return t.memberExpression(object, t.identifier(expr.property.name));
|
|
845
|
+
}
|
|
846
|
+
// ============================================================================
|
|
847
|
+
// Attribute Transformation
|
|
848
|
+
// ============================================================================
|
|
849
|
+
/** Returns true when `name` can be used as an unquoted JS identifier. */
|
|
850
|
+
function isValidJSIdentifier(name) {
|
|
851
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
|
852
|
+
}
|
|
853
|
+
function transformAttributes(attributes, state, isNativeElement = true, tagName = '') {
|
|
854
|
+
const props = [];
|
|
855
|
+
const spreads = [];
|
|
856
|
+
const cfValueProp = CONTROL_FLOW_VALUE_PROPS[tagName];
|
|
857
|
+
for (const attr of attributes) {
|
|
858
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
859
|
+
// Collect spread expressions separately
|
|
860
|
+
spreads.push(attr.argument);
|
|
861
|
+
}
|
|
862
|
+
else if (t.isJSXAttribute(attr)) {
|
|
863
|
+
const name = getAttributeName(attr.name);
|
|
864
|
+
const value = transformAttributeValue(attr.value, state, name, isNativeElement);
|
|
865
|
+
// Detect event handlers for delegation
|
|
866
|
+
if (EVENT_PROPS.test(name)) {
|
|
867
|
+
const eventName = name.slice(2).toLowerCase();
|
|
868
|
+
if (DELEGATABLE_EVENTS.includes(eventName)) {
|
|
869
|
+
state.delegatedEvents.add(eventName);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const key = isValidJSIdentifier(name)
|
|
873
|
+
? t.identifier(name)
|
|
874
|
+
: t.stringLiteral(name);
|
|
875
|
+
// Inline JSX passed to a COMPONENT prop (e.g. <Suspense fallback={<div/>}>) becomes a
|
|
876
|
+
// LAZY getter — same as children. Otherwise it's an eager IIFE built when the props object
|
|
877
|
+
// is constructed; during hydration that walks the hydration cursor and adopts the real
|
|
878
|
+
// server node (a fallback `<div>` matches the content `<div>` by tag), duplicating the
|
|
879
|
+
// tree. As a getter the node is only built when the component actually reads the prop.
|
|
880
|
+
const raw = attr.value;
|
|
881
|
+
const isInlineJsx = t.isJSXElement(raw) ||
|
|
882
|
+
t.isJSXFragment(raw) ||
|
|
883
|
+
(t.isJSXExpressionContainer(raw) &&
|
|
884
|
+
(t.isJSXElement(raw.expression) || t.isJSXFragment(raw.expression)));
|
|
885
|
+
// Control-flow reactive value prop (each/when/component/mount): emit as a getter so
|
|
886
|
+
// reading it re-runs the caller's expression inside the component's tracking scope.
|
|
887
|
+
const isCfValueProp = !isNativeElement && name === cfValueProp;
|
|
888
|
+
// A reactive component prop (`prop={sig()}`, `prop={store.x}`) must be a getter, not an
|
|
889
|
+
// eager value: eager reads the signal inside the enclosing `createMemo(() =>
|
|
890
|
+
// createComponent(Comp, props))`, so the memo re-creates the WHOLE component on every
|
|
891
|
+
// change (a <Portal> then re-mounts and piles up). As a getter the component is created
|
|
892
|
+
// once and reactivity flows through the getter into the component's own effects.
|
|
893
|
+
const rawExpr = t.isJSXExpressionContainer(raw) && !t.isJSXEmptyExpression(raw.expression)
|
|
894
|
+
? raw.expression
|
|
895
|
+
: null;
|
|
896
|
+
const isReactiveComponentProp = !isNativeElement && !!rawExpr && shouldAutoWrapAttrExpression(rawExpr, name);
|
|
897
|
+
if ((!isNativeElement && isInlineJsx) || isCfValueProp || isReactiveComponentProp) {
|
|
898
|
+
props.push(t.objectMethod('get', key, [], t.blockStatement([t.returnStatement(value)])));
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
props.push(t.objectProperty(key, value));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return { props, spreads };
|
|
906
|
+
}
|
|
907
|
+
function getAttributeName(name) {
|
|
908
|
+
if (t.isJSXIdentifier(name)) {
|
|
909
|
+
return name.name === 'class' ? 'className' : name.name;
|
|
910
|
+
}
|
|
911
|
+
if (t.isJSXNamespacedName(name)) {
|
|
912
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
913
|
+
}
|
|
914
|
+
return 'unknown';
|
|
915
|
+
}
|
|
916
|
+
function transformAttributeValue(value, state, attrName = '', isNativeElement = true) {
|
|
917
|
+
if (value === null) {
|
|
918
|
+
return t.booleanLiteral(true);
|
|
919
|
+
}
|
|
920
|
+
if (t.isStringLiteral(value)) {
|
|
921
|
+
// JSX allows string attribute values to span multiple lines, but JS string
|
|
922
|
+
// literals cannot contain raw newlines. When multi-line, normalise: collapse
|
|
923
|
+
// any run of whitespace (including newlines) to a single space and trim.
|
|
924
|
+
// We create a *new* StringLiteral so Babel discards the original extra.raw
|
|
925
|
+
// (which still contains the literal newlines) and generates a clean literal.
|
|
926
|
+
if (value.value.includes('\n')) {
|
|
927
|
+
return t.stringLiteral(value.value.replace(/\s+/g, ' ').trim());
|
|
928
|
+
}
|
|
929
|
+
return value;
|
|
930
|
+
}
|
|
931
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
932
|
+
if (t.isJSXEmptyExpression(value.expression)) {
|
|
933
|
+
return t.booleanLiteral(true);
|
|
934
|
+
}
|
|
935
|
+
const expr = value.expression;
|
|
936
|
+
// Auto-wrap reactive expressions so DOM attribute reads happen inside effects.
|
|
937
|
+
// Only applies to native HTML elements — component props must NOT be wrapped
|
|
938
|
+
// because the component receives the value directly and calling it would give
|
|
939
|
+
// () => value instead of value.
|
|
940
|
+
if (isNativeElement && shouldAutoWrapAttrExpression(expr, attrName)) {
|
|
941
|
+
return t.arrowFunctionExpression([], expr);
|
|
942
|
+
}
|
|
943
|
+
return expr;
|
|
944
|
+
}
|
|
945
|
+
if (t.isJSXElement(value)) {
|
|
946
|
+
return transformAutomaticElement(value, state, false, state.autoShow ?? true);
|
|
947
|
+
}
|
|
948
|
+
if (t.isJSXFragment(value)) {
|
|
949
|
+
return transformJSXFragment(value, state, {
|
|
950
|
+
runtime: 'automatic',
|
|
951
|
+
pragmaFrag: 'Fragment',
|
|
952
|
+
development: false,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
return t.booleanLiteral(true);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Decide whether a JSX attribute value expression should be auto-wrapped
|
|
959
|
+
* in `() => expr` for fine-grained reactivity (SolidJS style).
|
|
960
|
+
*/
|
|
961
|
+
function shouldAutoWrapAttrExpression(expr, attrName) {
|
|
962
|
+
// Event handlers must NOT be wrapped — they are called by the runtime.
|
|
963
|
+
// Covers both camelCase (onClick) and namespaced (on:click) forms.
|
|
964
|
+
if (EVENT_PROPS.test(attrName) || attrName.startsWith('on:')) {
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
// ref receives the DOM element directly — never wrap.
|
|
968
|
+
if (attrName === 'ref') {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
// use:Directive — the directive itself decides how to handle the value.
|
|
972
|
+
if (attrName.startsWith('use:')) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
// Already an arrow function or function expression — already reactive.
|
|
976
|
+
if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
// Plain identifier — may itself be a signal accessor/function; let runtime handle it.
|
|
980
|
+
if (t.isIdentifier(expr)) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
// Static literals need no wrapping.
|
|
984
|
+
if (t.isStringLiteral(expr) ||
|
|
985
|
+
t.isNumericLiteral(expr) ||
|
|
986
|
+
t.isBooleanLiteral(expr) ||
|
|
987
|
+
t.isNullLiteral(expr)) {
|
|
988
|
+
return false;
|
|
989
|
+
}
|
|
990
|
+
// Call expressions: signal(), count(), store.method()
|
|
991
|
+
if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
// Member expressions: store.prop, obj.value
|
|
995
|
+
if (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
// Compound expressions that may contain reactive reads.
|
|
999
|
+
if (t.isLogicalExpression(expr) ||
|
|
1000
|
+
t.isConditionalExpression(expr) ||
|
|
1001
|
+
t.isBinaryExpression(expr) ||
|
|
1002
|
+
t.isUnaryExpression(expr)) {
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
// Template literals: `Hello ${name()}`
|
|
1006
|
+
if (t.isTemplateLiteral(expr)) {
|
|
1007
|
+
return true;
|
|
1008
|
+
}
|
|
1009
|
+
// Object expressions: classList={{ active: isActive() }}
|
|
1010
|
+
if (t.isObjectExpression(expr)) {
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
// Array expressions: style={[baseStyle, dynamicStyle()]}
|
|
1014
|
+
if (t.isArrayExpression(expr)) {
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
// ============================================================================
|
|
1020
|
+
// Children Transformation
|
|
1021
|
+
// ============================================================================
|
|
1022
|
+
/**
|
|
1023
|
+
* Canonical JSX text cleaning (matches babel/React/Solid): trim line-boundary
|
|
1024
|
+
* whitespace, collapse runs to single spaces, and drop text that is only
|
|
1025
|
+
* whitespace spanning a newline. Returns '' when the node is insignificant.
|
|
1026
|
+
*/
|
|
1027
|
+
function cleanJSXText(value) {
|
|
1028
|
+
const lines = value.split(/\r\n|\n|\r/);
|
|
1029
|
+
let lastNonEmpty = 0;
|
|
1030
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1031
|
+
if (/[^ \t]/.test(lines[i]))
|
|
1032
|
+
lastNonEmpty = i;
|
|
1033
|
+
}
|
|
1034
|
+
let out = '';
|
|
1035
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1036
|
+
let line = lines[i].replace(/\t/g, ' ');
|
|
1037
|
+
if (i !== 0)
|
|
1038
|
+
line = line.replace(/^ +/, '');
|
|
1039
|
+
if (i !== lines.length - 1)
|
|
1040
|
+
line = line.replace(/ +$/, '');
|
|
1041
|
+
if (!line)
|
|
1042
|
+
continue;
|
|
1043
|
+
if (i !== lastNonEmpty)
|
|
1044
|
+
line += ' ';
|
|
1045
|
+
out += line;
|
|
1046
|
+
}
|
|
1047
|
+
return out;
|
|
1048
|
+
}
|
|
1049
|
+
function transformChildren(children, state, autoShow = true) {
|
|
1050
|
+
const result = [];
|
|
1051
|
+
for (const child of children) {
|
|
1052
|
+
if (t.isJSXText(child)) {
|
|
1053
|
+
// Canonical JSX whitespace handling (as React/Solid): whitespace-only text
|
|
1054
|
+
// that spans a line break is insignificant and dropped; inline runs collapse
|
|
1055
|
+
// to a single space. This matters beyond cosmetics — keeping a `" "` text
|
|
1056
|
+
// node next to a child turns `props.children` into an ARRAY, and an array
|
|
1057
|
+
// insert doesn't reactively bind a memo/component sitting inside it (so a
|
|
1058
|
+
// nested lazy/<Suspense> never updates). Dropping the stray whitespace keeps
|
|
1059
|
+
// a lone child a lone child, so insert binds it reactively.
|
|
1060
|
+
const text = cleanJSXText(child.value);
|
|
1061
|
+
if (text)
|
|
1062
|
+
result.push(t.stringLiteral(text));
|
|
1063
|
+
}
|
|
1064
|
+
else if (t.isJSXExpressionContainer(child)) {
|
|
1065
|
+
if (!t.isJSXEmptyExpression(child.expression)) {
|
|
1066
|
+
const expr = child.expression;
|
|
1067
|
+
// Auto-transform patterns into jsx(Show, ...) so JSX subtrees are
|
|
1068
|
+
// created once and never recreated on every signal change.
|
|
1069
|
+
//
|
|
1070
|
+
// Handled patterns:
|
|
1071
|
+
// () => cond && <JSX /> — arrow logical
|
|
1072
|
+
// () => cond ? <JSX /> : fallback — arrow ternary
|
|
1073
|
+
// cond && <JSX /> — bare logical
|
|
1074
|
+
// cond ? <JSX /> : fallback — bare ternary
|
|
1075
|
+
if (autoShow) {
|
|
1076
|
+
const showNode = tryTransformArrowToShow(expr, state) ??
|
|
1077
|
+
tryTransformRawToShow(expr, state);
|
|
1078
|
+
if (showNode) {
|
|
1079
|
+
result.push(showNode);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
// autoShow off: wrap JSX conditional expressions in createMemo(() => expr)
|
|
1085
|
+
// so the condition is still evaluated reactively without recreating the tree.
|
|
1086
|
+
const memoNode = tryWrapConditionalAsMemo(expr, state);
|
|
1087
|
+
if (memoNode) {
|
|
1088
|
+
result.push(memoNode);
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// Auto-wrap reactive expressions for SolidJS-style reactivity
|
|
1093
|
+
if (shouldAutoWrapExpression(expr)) {
|
|
1094
|
+
result.push(t.arrowFunctionExpression([], expr));
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
result.push(expr);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
else if (t.isJSXElement(child)) {
|
|
1102
|
+
result.push(transformAutomaticElement(child, state, false, state.autoShow ?? true));
|
|
1103
|
+
}
|
|
1104
|
+
else if (t.isJSXFragment(child)) {
|
|
1105
|
+
result.push(transformJSXFragment(child, state, {
|
|
1106
|
+
runtime: 'automatic',
|
|
1107
|
+
pragmaFrag: 'Fragment',
|
|
1108
|
+
development: false,
|
|
1109
|
+
}));
|
|
1110
|
+
}
|
|
1111
|
+
else if (t.isJSXSpreadChild(child)) {
|
|
1112
|
+
result.push(child.expression);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
// ============================================================================
|
|
1118
|
+
// Auto-Show Transform
|
|
1119
|
+
// ============================================================================
|
|
1120
|
+
/**
|
|
1121
|
+
* Detects the patterns:
|
|
1122
|
+
* () => condition && <JSX />
|
|
1123
|
+
* () => condition ? <JSX /> : null
|
|
1124
|
+
* () => condition ? <JSX /> : <Fallback />
|
|
1125
|
+
*
|
|
1126
|
+
* and rewrites them to:
|
|
1127
|
+
* jsx(Show, { when: () => condition, children: jsx(...), fallback? })
|
|
1128
|
+
*
|
|
1129
|
+
* This prevents the JSX subtree from being recreated on every reactive update.
|
|
1130
|
+
* Show's memo only re-evaluates when the boolean result of `condition` changes.
|
|
1131
|
+
*/
|
|
1132
|
+
function tryTransformArrowToShow(expr, state) {
|
|
1133
|
+
if (!t.isArrowFunctionExpression(expr) ||
|
|
1134
|
+
expr.params.length !== 0 ||
|
|
1135
|
+
expr.async) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
const body = expr.body;
|
|
1139
|
+
// Pattern 1: () => condition && <JSX /> (or () => condition && (() => <JSX />))
|
|
1140
|
+
if (t.isLogicalExpression(body) &&
|
|
1141
|
+
body.operator === '&&') {
|
|
1142
|
+
const jsx = getJSXContent(body.right);
|
|
1143
|
+
if (jsx) {
|
|
1144
|
+
return buildShowCall(unwrapZeroArgArrow(body.left), jsx, null, state);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Pattern 2: () => condition ? <JSX /> : null | undefined | false
|
|
1148
|
+
// Pattern 3: () => condition ? <JSX /> : <Fallback />
|
|
1149
|
+
// Pattern 4: () => condition ? <JSX /> : otherCond ? <JSX2 /> : <Fallback /> (nested ternary)
|
|
1150
|
+
if (t.isConditionalExpression(body)) {
|
|
1151
|
+
const jsx = getJSXContent(body.consequent);
|
|
1152
|
+
if (jsx) {
|
|
1153
|
+
const fallback = buildFallback(body.alternate, state);
|
|
1154
|
+
return buildShowCall(unwrapZeroArgArrow(body.test), jsx, fallback, state, true);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return null;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Handles bare (non-arrow-wrapped) ternary and logical JSX expressions:
|
|
1161
|
+
*
|
|
1162
|
+
* cond && <JSX />
|
|
1163
|
+
* cond ? <JSX /> : fallback
|
|
1164
|
+
*
|
|
1165
|
+
* These are transformed to `jsx(Show, { when: () => cond, ... })` exactly like
|
|
1166
|
+
* their arrow-wrapped counterparts, preventing JSX subtrees from being
|
|
1167
|
+
* recreated on every reactive update.
|
|
1168
|
+
*/
|
|
1169
|
+
function tryTransformRawToShow(expr, state) {
|
|
1170
|
+
// cond && <JSX /> (or cond && (() => <JSX />))
|
|
1171
|
+
if (t.isLogicalExpression(expr) && expr.operator === '&&') {
|
|
1172
|
+
const jsx = getJSXContent(expr.right);
|
|
1173
|
+
if (jsx) {
|
|
1174
|
+
return buildShowCall(unwrapZeroArgArrow(expr.left), jsx, null, state);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
// cond ? <JSX /> : fallback (consequent may be () => <JSX />)
|
|
1178
|
+
// test may be (() => expr) — unwrap to avoid double arrow wrapping
|
|
1179
|
+
if (t.isConditionalExpression(expr)) {
|
|
1180
|
+
const jsx = getJSXContent(expr.consequent);
|
|
1181
|
+
if (jsx) {
|
|
1182
|
+
const fallback = buildFallback(expr.alternate, state);
|
|
1183
|
+
return buildShowCall(unwrapZeroArgArrow(expr.test), jsx, fallback, state);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return null;
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* When autoShow is disabled, wraps ALL ternary and logical-AND expressions in
|
|
1190
|
+
* `createMemo(() => normalizedExpr)` so the condition evaluates reactively and
|
|
1191
|
+
* the result is cached (not re-evaluated on every render cycle).
|
|
1192
|
+
*
|
|
1193
|
+
* Normalization rules applied before wrapping:
|
|
1194
|
+
* - Outer zero-arg arrow unwrapped: `() => cond ? a : b` → body used directly
|
|
1195
|
+
* - Arrow-wrapped test unwrapped: `(() => cond) ? a : b` → `cond ? a : b`
|
|
1196
|
+
* - Arrow-wrapped JSX consequent: `() => <JSX>` → `<JSX>` (so Babel transforms it)
|
|
1197
|
+
* - Arrow-wrapped JSX alternate: `() => <JSX>` → `<JSX>` likewise
|
|
1198
|
+
* - Non-JSX branches left as-is: strings, identifiers, call results, etc.
|
|
1199
|
+
*
|
|
1200
|
+
* Examples:
|
|
1201
|
+
* {cond ? "a" : "b"} → createMemo(() => cond ? "a" : "b")
|
|
1202
|
+
* {cond ? <A/> : <B/>} → createMemo(() => cond ? <A/> : <B/>)
|
|
1203
|
+
* {(() => cond) ? ()=><A/> : <B/>} → createMemo(() => cond ? <A/> : <B/>)
|
|
1204
|
+
* {cond && <A/>} → createMemo(() => cond && <A/>)
|
|
1205
|
+
*/
|
|
1206
|
+
function tryWrapConditionalAsMemo(expr, state) {
|
|
1207
|
+
// Unwrap outer zero-arg arrow: () => <inner> → work on inner directly
|
|
1208
|
+
const inner = t.isArrowFunctionExpression(expr) &&
|
|
1209
|
+
expr.params.length === 0 &&
|
|
1210
|
+
!expr.async &&
|
|
1211
|
+
t.isExpression(expr.body)
|
|
1212
|
+
? expr.body
|
|
1213
|
+
: expr;
|
|
1214
|
+
if (t.isConditionalExpression(inner)) {
|
|
1215
|
+
// Unwrap arrow-wrapped test to prevent always-truthy condition
|
|
1216
|
+
const normalizedTest = unwrapZeroArgArrow(inner.test);
|
|
1217
|
+
// Unwrap arrow-wrapped JSX branches so Babel can transform them;
|
|
1218
|
+
// non-JSX branches (strings, calls, etc.) are kept as-is
|
|
1219
|
+
const consequentJsx = getJSXContent(inner.consequent);
|
|
1220
|
+
const normalizedConsequent = consequentJsx ?? inner.consequent;
|
|
1221
|
+
const altJsx = getJSXContent(inner.alternate);
|
|
1222
|
+
const normalizedAlternate = altJsx ?? inner.alternate;
|
|
1223
|
+
const normalized = t.conditionalExpression(normalizedTest, normalizedConsequent, normalizedAlternate);
|
|
1224
|
+
// If either branch is a control-flow component (For, Switch, Index, Show, …),
|
|
1225
|
+
// do NOT wrap in createMemo — return null so the caller falls through to the
|
|
1226
|
+
// plain `() => expr` path via shouldAutoWrapExpression.
|
|
1227
|
+
if (isControlFlowJSX(inner.consequent) ||
|
|
1228
|
+
isControlFlowJSX(inner.alternate)) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
state.needsCreateMemo = true;
|
|
1232
|
+
return t.callExpression(t.identifier('createMemo'), [
|
|
1233
|
+
t.arrowFunctionExpression([], normalized),
|
|
1234
|
+
]);
|
|
1235
|
+
}
|
|
1236
|
+
if (t.isLogicalExpression(inner) && inner.operator === '&&') {
|
|
1237
|
+
const normalizedLeft = unwrapZeroArgArrow(inner.left);
|
|
1238
|
+
const rightJsx = getJSXContent(inner.right);
|
|
1239
|
+
const normalizedRight = rightJsx ?? inner.right;
|
|
1240
|
+
const normalized = t.logicalExpression('&&', normalizedLeft, normalizedRight);
|
|
1241
|
+
// Same guard for logical && (e.g. {show && <For ...>})
|
|
1242
|
+
if (isControlFlowJSX(inner.right)) {
|
|
1243
|
+
return null;
|
|
1244
|
+
}
|
|
1245
|
+
state.needsCreateMemo = true;
|
|
1246
|
+
return t.callExpression(t.identifier('createMemo'), [
|
|
1247
|
+
t.arrowFunctionExpression([], normalized),
|
|
1248
|
+
]);
|
|
1249
|
+
}
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Recursively transforms a fallback expression into a reactive value.
|
|
1254
|
+
* If the expression is a nested ternary `cond ? <JSX /> : rest`, it is
|
|
1255
|
+
* transformed into a nested `Show` call so the condition is tracked reactively.
|
|
1256
|
+
*/
|
|
1257
|
+
function buildFallback(expr, state) {
|
|
1258
|
+
if (isFalsyNode(expr))
|
|
1259
|
+
return null;
|
|
1260
|
+
// bare JSX or () => <JSX>
|
|
1261
|
+
const directJsx = getJSXContent(expr);
|
|
1262
|
+
if (directJsx) {
|
|
1263
|
+
return transformJSXNode(directJsx, state);
|
|
1264
|
+
}
|
|
1265
|
+
// Nested ternary whose truthy branch is JSX (or arrow-wrapped JSX)
|
|
1266
|
+
if (t.isConditionalExpression(expr)) {
|
|
1267
|
+
const inner = expr;
|
|
1268
|
+
const innerJsx = getJSXContent(inner.consequent);
|
|
1269
|
+
if (innerJsx) {
|
|
1270
|
+
const nestedFallback = buildFallback(inner.alternate, state);
|
|
1271
|
+
return buildShowCall(unwrapZeroArgArrow(inner.test), innerJsx, nestedFallback, state);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return expr;
|
|
1275
|
+
}
|
|
1276
|
+
function isJSXNode(node) {
|
|
1277
|
+
return t.isJSXElement(node) || t.isJSXFragment(node);
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Returns true when `expr` (or a zero-arg arrow wrapping it) is a JSXElement
|
|
1281
|
+
* whose tag is one of the Fluixi control-flow components (For, Switch, Index,
|
|
1282
|
+
* Show, Portal, Dynamic, ErrorBoundary, Suspense). These must NOT be wrapped in
|
|
1283
|
+
* createMemo — a plain `() => expr` arrow is sufficient and avoids double-tracking.
|
|
1284
|
+
*/
|
|
1285
|
+
function isControlFlowJSX(expr) {
|
|
1286
|
+
const jsx = getJSXContent(expr);
|
|
1287
|
+
if (!jsx || !t.isJSXElement(jsx))
|
|
1288
|
+
return false;
|
|
1289
|
+
const name = jsx.openingElement.name;
|
|
1290
|
+
return t.isJSXIdentifier(name) && CONTROL_FLOW_COMPONENTS.has(name.name);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Returns the JSX content from either bare `<JSX>` or a zero-arg arrow `() => <JSX>`.
|
|
1294
|
+
* This allows Show transformation to work regardless of whether the author wrapped the
|
|
1295
|
+
* consequent/alternate in an arrow function or not.
|
|
1296
|
+
*/
|
|
1297
|
+
function getJSXContent(expr) {
|
|
1298
|
+
if (isJSXNode(expr))
|
|
1299
|
+
return expr;
|
|
1300
|
+
if (t.isArrowFunctionExpression(expr) &&
|
|
1301
|
+
expr.params.length === 0 &&
|
|
1302
|
+
!expr.async &&
|
|
1303
|
+
isJSXNode(expr.body)) {
|
|
1304
|
+
return expr.body;
|
|
1305
|
+
}
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Unwraps a zero-argument arrow `() => expr` to `expr`.
|
|
1310
|
+
* Used to avoid double-wrapping when the test/condition is already `() => cond`.
|
|
1311
|
+
* `buildShowCall` always wraps the condition in `() =>`, so passing a bare expr
|
|
1312
|
+
* produces `() => cond`; passing `() => cond` without unwrapping produces `() => () => cond`
|
|
1313
|
+
* which is always truthy.
|
|
1314
|
+
*/
|
|
1315
|
+
function unwrapZeroArgArrow(expr) {
|
|
1316
|
+
if (t.isArrowFunctionExpression(expr) &&
|
|
1317
|
+
expr.params.length === 0 &&
|
|
1318
|
+
!expr.async &&
|
|
1319
|
+
t.isExpression(expr.body)) {
|
|
1320
|
+
return expr.body;
|
|
1321
|
+
}
|
|
1322
|
+
return expr;
|
|
1323
|
+
}
|
|
1324
|
+
function isFalsyNode(node) {
|
|
1325
|
+
return (t.isNullLiteral(node) ||
|
|
1326
|
+
(t.isIdentifier(node) && node.name === 'undefined') ||
|
|
1327
|
+
(t.isBooleanLiteral(node) && node.value === false));
|
|
1328
|
+
}
|
|
1329
|
+
function transformJSXNode(node, state) {
|
|
1330
|
+
if (t.isJSXElement(node)) {
|
|
1331
|
+
return transformAutomaticElement(node, state, false, state.autoShow ?? true);
|
|
1332
|
+
}
|
|
1333
|
+
return transformJSXFragment(node, state, {
|
|
1334
|
+
runtime: 'automatic',
|
|
1335
|
+
pragmaFrag: 'Fragment',
|
|
1336
|
+
development: false,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function buildShowCall(condition, children, fallback, state, fromArrow = false) {
|
|
1340
|
+
// Register Show so it gets auto-imported if needed
|
|
1341
|
+
state.controlFlowComponents.add('Show');
|
|
1342
|
+
state.needsControlFlowImport = true;
|
|
1343
|
+
const transformedChildren = transformJSXNode(children, state);
|
|
1344
|
+
// console.log('build show call', generate(condition).code);
|
|
1345
|
+
const props = [
|
|
1346
|
+
t.objectProperty(t.identifier('when'), t.arrowFunctionExpression([], condition)
|
|
1347
|
+
// fromArrow ? (condition) : t.arrowFunctionExpression([], condition),
|
|
1348
|
+
),
|
|
1349
|
+
t.objectProperty(t.identifier('children'), transformedChildren),
|
|
1350
|
+
];
|
|
1351
|
+
if (fallback) {
|
|
1352
|
+
props.push(t.objectProperty(t.identifier('fallback'), fallback));
|
|
1353
|
+
}
|
|
1354
|
+
return emitComponentCall(t.identifier('Show'), t.objectExpression(props), state);
|
|
1355
|
+
}
|
|
1356
|
+
// ============================================================================
|
|
1357
|
+
// Reactivity Detection
|
|
1358
|
+
// ============================================================================
|
|
1359
|
+
/**
|
|
1360
|
+
* Check if an expression should be auto-wrapped in an arrow function for reactivity
|
|
1361
|
+
* This enables SolidJS-style reactivity where you can write {store.property} or {signal()}
|
|
1362
|
+
* without manual wrapping
|
|
1363
|
+
*/
|
|
1364
|
+
function shouldAutoWrapExpression(expr) {
|
|
1365
|
+
// Member expressions: store.property, user.name, etc.
|
|
1366
|
+
// These need wrapping so property access happens inside an effect
|
|
1367
|
+
if (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
|
|
1368
|
+
// `props.children` (any `.children`) is a stable subtree reference, not a
|
|
1369
|
+
// reactive value. Wrapping it in `() => props.children` adds an insert layer
|
|
1370
|
+
// that fully unwraps the children's Suspense/memo to a static node, severing
|
|
1371
|
+
// the reactive binding — so a nested lazy/<Suspense> that resolves later
|
|
1372
|
+
// never propagates up through the component-children hop. Pass it through
|
|
1373
|
+
// non-reactive (insert(el, props.children)) so insert binds to the child's
|
|
1374
|
+
// own memo, exactly like a directly-rendered <Outlet/>.
|
|
1375
|
+
if (!expr.computed && t.isIdentifier(expr.property, { name: 'children' })) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
// Call expressions: signal(), count(), etc.
|
|
1381
|
+
// These are already function calls but need wrapping to re-run on updates
|
|
1382
|
+
if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
|
|
1383
|
+
return true;
|
|
1384
|
+
}
|
|
1385
|
+
// DON'T wrap plain identifiers - they might be signal accessors or functions
|
|
1386
|
+
// that should be passed as-is. Only wrap when accessing properties or calling.
|
|
1387
|
+
if (t.isIdentifier(expr)) {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
// Logical expressions: signal() && other, store.value || default
|
|
1391
|
+
if (t.isLogicalExpression(expr) || t.isConditionalExpression(expr)) {
|
|
1392
|
+
return true;
|
|
1393
|
+
}
|
|
1394
|
+
// Binary expressions: signal() + 1, store.count * 2
|
|
1395
|
+
if (t.isBinaryExpression(expr)) {
|
|
1396
|
+
return true;
|
|
1397
|
+
}
|
|
1398
|
+
// Unary expressions: !signal(), -store.count
|
|
1399
|
+
if (t.isUnaryExpression(expr)) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
// Template literals: `Count: ${count()}`
|
|
1403
|
+
if (t.isTemplateLiteral(expr)) {
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
// Don't wrap literals, strings, numbers, etc.
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
// ============================================================================
|
|
1410
|
+
// Static Optimization
|
|
1411
|
+
// ============================================================================
|
|
1412
|
+
function shouldWrapReactive(expr, state) {
|
|
1413
|
+
// Call expressions ending with () - likely signal accessors
|
|
1414
|
+
if (t.isCallExpression(expr) && t.isIdentifier(expr.callee)) {
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
// Arrow functions with no parameters - could be computed values
|
|
1418
|
+
if (t.isArrowFunctionExpression(expr) && expr.params.length === 0) {
|
|
1419
|
+
return true;
|
|
1420
|
+
}
|
|
1421
|
+
// Function expressions with no parameters
|
|
1422
|
+
if (t.isFunctionExpression(expr) && expr.params.length === 0) {
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
// Member expressions - could be store access
|
|
1426
|
+
if (t.isMemberExpression(expr)) {
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
function wrapReactiveExpression(expr) {
|
|
1432
|
+
return t.callExpression(t.identifier(''), [
|
|
1433
|
+
t.arrowFunctionExpression([], expr),
|
|
1434
|
+
]);
|
|
1435
|
+
}
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
// Static Element Optimization
|
|
1438
|
+
// ============================================================================
|
|
1439
|
+
function isStaticElement(element, state) {
|
|
1440
|
+
const openingElement = element.openingElement;
|
|
1441
|
+
// Check if tag is static (string literal)
|
|
1442
|
+
if (!t.isJSXIdentifier(openingElement.name)) {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
const tagName = openingElement.name.name;
|
|
1446
|
+
if (tagName[0] !== tagName[0].toLowerCase()) {
|
|
1447
|
+
// Component, not a native element
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
// Check if all attributes are static
|
|
1451
|
+
for (const attr of openingElement.attributes) {
|
|
1452
|
+
if (t.isJSXSpreadAttribute(attr)) {
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
if (t.isJSXAttribute(attr)) {
|
|
1456
|
+
const value = attr.value;
|
|
1457
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
1458
|
+
if (!t.isStringLiteral(value.expression) &&
|
|
1459
|
+
!t.isNumericLiteral(value.expression)) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
// Check if all children are static
|
|
1466
|
+
for (const child of element.children) {
|
|
1467
|
+
if (t.isJSXElement(child) && !isStaticElement(child, state)) {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
if (t.isJSXExpressionContainer(child)) {
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
function hoistStaticElement(element, state) {
|
|
1477
|
+
const varName = `_$static${state.staticCounter++}`;
|
|
1478
|
+
const transformed = transformAutomaticElement(element, state, false);
|
|
1479
|
+
state.staticElements.set(varName, transformed);
|
|
1480
|
+
return t.identifier(varName);
|
|
1481
|
+
}
|
|
1482
|
+
// ============================================================================
|
|
1483
|
+
// Control Flow Optimization
|
|
1484
|
+
// ============================================================================
|
|
1485
|
+
function optimizeControlFlowComponent(element, state) {
|
|
1486
|
+
const componentName = element.openingElement.name.name;
|
|
1487
|
+
// Optimize Show component
|
|
1488
|
+
if (componentName === 'Show') {
|
|
1489
|
+
return optimizeShowComponent(element, state);
|
|
1490
|
+
}
|
|
1491
|
+
// Optimize For component
|
|
1492
|
+
if (componentName === 'For') {
|
|
1493
|
+
return optimizeForComponent(element, state);
|
|
1494
|
+
}
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
function optimizeShowComponent(element, state) {
|
|
1498
|
+
// Extract when, fallback, and children
|
|
1499
|
+
// Apply optimizations based on patterns
|
|
1500
|
+
return null; // Placeholder for optimization logic
|
|
1501
|
+
}
|
|
1502
|
+
function optimizeForComponent(element, state) {
|
|
1503
|
+
// Extract each and children
|
|
1504
|
+
// Apply keyed reconciliation optimizations
|
|
1505
|
+
return null; // Placeholder for optimization logic
|
|
1506
|
+
}
|
|
1507
|
+
// ============================================================================
|
|
1508
|
+
// Exports
|
|
1509
|
+
// ============================================================================
|