@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,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Bundler integrations for the Fluixi compiler.
|
|
3
|
+
* @module @fluixi/compiler/integrations
|
|
4
|
+
*
|
|
5
|
+
* Build-tool adapters (vite/webpack/rollup/esbuild) plus the prop/template
|
|
6
|
+
* analysis helpers that drive them. These wrap the babel front-end so a single
|
|
7
|
+
* package owns all compile-time code. @fluixi/core re-exports
|
|
8
|
+
* createVitePlugin from here.
|
|
9
|
+
*
|
|
10
|
+
* Helpers:
|
|
11
|
+
* - Detecting reactive expressions
|
|
12
|
+
* - Wrapping signals for optimal updates
|
|
13
|
+
* - Template optimization
|
|
14
|
+
* - Static vs dynamic prop detection
|
|
15
|
+
*/
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Constants
|
|
18
|
+
// ============================================================================
|
|
19
|
+
const STATIC_PROPS = new Set(['key', 'ref', 'children']);
|
|
20
|
+
const REACTIVE_PATTERNS = [
|
|
21
|
+
// Signal accessor patterns
|
|
22
|
+
/\(\)\s*=>/, // Arrow function
|
|
23
|
+
/function\s*\(\)/, // Function expression
|
|
24
|
+
/\w+\(\)/, // Function call that might be accessor
|
|
25
|
+
// Store patterns
|
|
26
|
+
/\w+\.\w+/, // Property access (might be store proxy)
|
|
27
|
+
/\w+\[.+\]/, // Bracket notation
|
|
28
|
+
// Reactive operators
|
|
29
|
+
/createSignal/,
|
|
30
|
+
/createStore/,
|
|
31
|
+
/createMemo/,
|
|
32
|
+
/createEffect/,
|
|
33
|
+
];
|
|
34
|
+
const EVENT_PATTERN = /^on[A-Z]/;
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Prop Analysis
|
|
37
|
+
// ============================================================================
|
|
38
|
+
/**
|
|
39
|
+
* Analyze a prop to determine if it's static or dynamic
|
|
40
|
+
*/
|
|
41
|
+
export function analyzeProp(name, value) {
|
|
42
|
+
const analysis = {
|
|
43
|
+
name,
|
|
44
|
+
isStatic: true,
|
|
45
|
+
isReactive: false,
|
|
46
|
+
isEvent: EVENT_PATTERN.test(name),
|
|
47
|
+
isRef: name === 'ref',
|
|
48
|
+
isSpread: name.startsWith('...'),
|
|
49
|
+
};
|
|
50
|
+
// Static props are always static
|
|
51
|
+
if (STATIC_PROPS.has(name)) {
|
|
52
|
+
return analysis;
|
|
53
|
+
}
|
|
54
|
+
// Events and refs are handled specially
|
|
55
|
+
if (analysis.isEvent || analysis.isRef) {
|
|
56
|
+
analysis.isStatic = false;
|
|
57
|
+
return analysis;
|
|
58
|
+
}
|
|
59
|
+
// Check if value appears reactive
|
|
60
|
+
if (typeof value === 'string') {
|
|
61
|
+
const code = value;
|
|
62
|
+
// Check for reactive patterns
|
|
63
|
+
for (const pattern of REACTIVE_PATTERNS) {
|
|
64
|
+
if (pattern.test(code)) {
|
|
65
|
+
analysis.isStatic = false;
|
|
66
|
+
analysis.isReactive = true;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Check for template literals with expressions
|
|
71
|
+
if (code.includes('${')) {
|
|
72
|
+
analysis.isStatic = false;
|
|
73
|
+
analysis.isReactive = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (typeof value === 'function') {
|
|
77
|
+
// Functions are potentially reactive
|
|
78
|
+
analysis.isStatic = false;
|
|
79
|
+
analysis.isReactive = true;
|
|
80
|
+
}
|
|
81
|
+
else if (typeof value === 'object' && value !== null) {
|
|
82
|
+
// Objects might contain reactive values
|
|
83
|
+
analysis.isStatic = false;
|
|
84
|
+
}
|
|
85
|
+
return analysis;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Analyze all props on an element
|
|
89
|
+
*/
|
|
90
|
+
export function analyzeProps(props) {
|
|
91
|
+
const analyses = new Map();
|
|
92
|
+
for (const [name, value] of Object.entries(props)) {
|
|
93
|
+
analyses.set(name, analyzeProp(name, value));
|
|
94
|
+
}
|
|
95
|
+
return analyses;
|
|
96
|
+
}
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// Template Detection
|
|
99
|
+
// ============================================================================
|
|
100
|
+
/**
|
|
101
|
+
* Check if JSX should be compiled to lit-html template
|
|
102
|
+
*/
|
|
103
|
+
export function shouldUseLitTemplate(tag, props, children) {
|
|
104
|
+
// Native elements can use lit-html
|
|
105
|
+
if (typeof tag !== 'string') {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
// Check if all props are compatible with lit-html
|
|
109
|
+
const propAnalyses = analyzeProps(props);
|
|
110
|
+
for (const analysis of propAnalyses.values()) {
|
|
111
|
+
// Spread props require native JSX
|
|
112
|
+
if (analysis.isSpread) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Complex reactive props might be better in native JSX
|
|
116
|
+
if (analysis.isReactive && !analysis.isEvent) {
|
|
117
|
+
// Allow simple reactive bindings in lit-html
|
|
118
|
+
// but complex ones are better in native JSX
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Check if a value is a lit-html template
|
|
125
|
+
*/
|
|
126
|
+
export function isLitTemplate(value) {
|
|
127
|
+
return value && typeof value === 'object' && '_$litType$' in value;
|
|
128
|
+
}
|
|
129
|
+
// ============================================================================
|
|
130
|
+
// Code Generation Helpers
|
|
131
|
+
// ============================================================================
|
|
132
|
+
/**
|
|
133
|
+
* Generate code for creating an element
|
|
134
|
+
*/
|
|
135
|
+
export function generateCreateElement(tag, props, children, options = {}) {
|
|
136
|
+
const isSVG = ['svg', 'path', 'circle', 'rect', 'g'].includes(tag);
|
|
137
|
+
// Generate props code
|
|
138
|
+
const propsCode = generatePropsCode(props, options);
|
|
139
|
+
// Generate children code
|
|
140
|
+
const childrenCode = children.length > 0 ? `, ${generateChildrenCode(children)}` : '';
|
|
141
|
+
if (options.useLitHTML !== false &&
|
|
142
|
+
shouldUseLitTemplate(tag, props, children)) {
|
|
143
|
+
return generateLitHTMLTemplate(tag, props, children, isSVG);
|
|
144
|
+
}
|
|
145
|
+
return `jsx('${tag}', ${propsCode}${childrenCode})`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generate code for props object
|
|
149
|
+
*/
|
|
150
|
+
export function generatePropsCode(props, options = {}) {
|
|
151
|
+
if (Object.keys(props).length === 0) {
|
|
152
|
+
return '{}';
|
|
153
|
+
}
|
|
154
|
+
const propAnalyses = analyzeProps(props);
|
|
155
|
+
const entries = [];
|
|
156
|
+
for (const [name, value] of Object.entries(props)) {
|
|
157
|
+
const analysis = propAnalyses.get(name);
|
|
158
|
+
if (analysis.isReactive && !analysis.isStatic) {
|
|
159
|
+
// Wrap reactive values
|
|
160
|
+
entries.push(`${name}: () => ${value}`);
|
|
161
|
+
}
|
|
162
|
+
else if (typeof value === 'string') {
|
|
163
|
+
entries.push(`${name}: "${value}"`);
|
|
164
|
+
}
|
|
165
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
166
|
+
entries.push(`${name}: ${value}`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
entries.push(`${name}: ${value}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return `{ ${entries.join(', ')} }`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Generate code for children
|
|
176
|
+
*/
|
|
177
|
+
export function generateChildrenCode(children) {
|
|
178
|
+
if (children.length === 0) {
|
|
179
|
+
return 'undefined';
|
|
180
|
+
}
|
|
181
|
+
if (children.length === 1) {
|
|
182
|
+
return String(children[0]);
|
|
183
|
+
}
|
|
184
|
+
return `[${children.join(', ')}]`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Generate lit-html template code
|
|
188
|
+
*/
|
|
189
|
+
export function generateLitHTMLTemplate(tag, props, children, isSVG = false) {
|
|
190
|
+
const templateFunc = isSVG ? 'svg' : 'html';
|
|
191
|
+
// Build template string
|
|
192
|
+
let template = `<${tag}`;
|
|
193
|
+
const values = [];
|
|
194
|
+
// Add props
|
|
195
|
+
for (const [name, value] of Object.entries(props)) {
|
|
196
|
+
if (name === 'children' || name === 'key' || name === 'ref') {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const analysis = analyzeProp(name, value);
|
|
200
|
+
if (analysis.isEvent) {
|
|
201
|
+
const eventName = name.slice(2).toLowerCase();
|
|
202
|
+
template += ` @${eventName}=\${${values.length}}`;
|
|
203
|
+
values.push(String(value));
|
|
204
|
+
}
|
|
205
|
+
else if (analysis.isReactive) {
|
|
206
|
+
template += ` ${name}=\${${values.length}}`;
|
|
207
|
+
values.push(`signal(() => ${value})`);
|
|
208
|
+
}
|
|
209
|
+
else if (typeof value === 'string') {
|
|
210
|
+
template += ` ${name}="${value}"`;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
template += ` ${name}=\${${values.length}}`;
|
|
214
|
+
values.push(String(value));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
template += '>';
|
|
218
|
+
// Add children
|
|
219
|
+
if (children.length > 0) {
|
|
220
|
+
for (const child of children) {
|
|
221
|
+
if (typeof child === 'string') {
|
|
222
|
+
template += child;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
template += `\${${values.length}}`;
|
|
226
|
+
values.push(String(child));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
template += `</${tag}>`;
|
|
231
|
+
// Generate template literal
|
|
232
|
+
return `${templateFunc}\`${template}\`${values.length > 0 ? `, ${values.join(', ')}` : ''}`;
|
|
233
|
+
}
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Optimization Passes
|
|
236
|
+
// ============================================================================
|
|
237
|
+
/**
|
|
238
|
+
* Optimize static content
|
|
239
|
+
*/
|
|
240
|
+
export function optimizeStatic(code) {
|
|
241
|
+
// Combine consecutive static strings
|
|
242
|
+
code = code.replace(/jsx\('([^']+)',\s*{},\s*"([^"]+)"\s*\)/g, (_, tag, text) => `_$staticNode('${tag}', '${text}')`);
|
|
243
|
+
return code;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Optimize prop spreads
|
|
247
|
+
*/
|
|
248
|
+
export function optimizeSpreads(code) {
|
|
249
|
+
// DISABLED: This string-based transformation was incorrectly transforming JSX spreads
|
|
250
|
+
// The babel plugin handles spread attributes properly using Object.assign
|
|
251
|
+
// This was causing spread(storeProxy) to be called instead of proper prop merging
|
|
252
|
+
return code;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Hoist static elements
|
|
256
|
+
*/
|
|
257
|
+
export function hoistStatic(code) {
|
|
258
|
+
const hoisted = [];
|
|
259
|
+
let counter = 0;
|
|
260
|
+
// Find static JSX elements
|
|
261
|
+
code = code.replace(/jsx\('([^']+)',\s*{([^}]*)},\s*([^)]*)\)/g, (match, tag, props, children) => {
|
|
262
|
+
// Simple heuristic: if no reactive props, hoist
|
|
263
|
+
if (!props.includes('()') && !props.includes('=>')) {
|
|
264
|
+
const varName = `_$static${counter++}`;
|
|
265
|
+
hoisted.push(`const ${varName} = ${match};`);
|
|
266
|
+
return varName;
|
|
267
|
+
}
|
|
268
|
+
return match;
|
|
269
|
+
});
|
|
270
|
+
if (hoisted.length > 0) {
|
|
271
|
+
return hoisted.join('\n') + '\n' + code;
|
|
272
|
+
}
|
|
273
|
+
return code;
|
|
274
|
+
}
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Event Delegation
|
|
277
|
+
// ============================================================================
|
|
278
|
+
/**
|
|
279
|
+
* Generate event delegation setup
|
|
280
|
+
*/
|
|
281
|
+
export function generateEventDelegation(events, options = {}) {
|
|
282
|
+
const delegatedEvents = options.delegatedEvents || [
|
|
283
|
+
'click',
|
|
284
|
+
'input',
|
|
285
|
+
'change',
|
|
286
|
+
'submit',
|
|
287
|
+
];
|
|
288
|
+
const filtered = events.filter((e) => delegatedEvents.includes(e.toLowerCase()));
|
|
289
|
+
if (filtered.length === 0) {
|
|
290
|
+
return '';
|
|
291
|
+
}
|
|
292
|
+
return `delegateEvents([${filtered.map((e) => `'${e}'`).join(', ')}]);`;
|
|
293
|
+
}
|
|
294
|
+
// ============================================================================
|
|
295
|
+
// Transform Pipeline
|
|
296
|
+
// ============================================================================
|
|
297
|
+
/**
|
|
298
|
+
* Transform JSX code with optimizations
|
|
299
|
+
*/
|
|
300
|
+
export function transform(code, options = {}) {
|
|
301
|
+
const metadata = {
|
|
302
|
+
hasSignals: /createSignal|signal\(/.test(code),
|
|
303
|
+
hasStores: /createStore|store\(/.test(code),
|
|
304
|
+
hasLitHTML: /html`|svg`/.test(code),
|
|
305
|
+
staticProps: new Set(),
|
|
306
|
+
dynamicProps: new Set(),
|
|
307
|
+
components: new Set(),
|
|
308
|
+
};
|
|
309
|
+
let transformed = code;
|
|
310
|
+
// Apply optimizations
|
|
311
|
+
if (options.optimize !== false) {
|
|
312
|
+
transformed = optimizeStatic(transformed);
|
|
313
|
+
// optimizeSpreads is disabled - babel plugin handles spreads correctly
|
|
314
|
+
// transformed = optimizeSpreads(transformed);
|
|
315
|
+
transformed = hoistStatic(transformed);
|
|
316
|
+
}
|
|
317
|
+
// Add event delegation if needed
|
|
318
|
+
if (options.delegateEvents !== false) {
|
|
319
|
+
const events = extractEvents(code);
|
|
320
|
+
const delegation = generateEventDelegation(events, options);
|
|
321
|
+
if (delegation) {
|
|
322
|
+
transformed = delegation + '\n' + transformed;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Add imports if needed
|
|
326
|
+
transformed = addImports(transformed, metadata, options);
|
|
327
|
+
return {
|
|
328
|
+
code: transformed,
|
|
329
|
+
metadata,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extract event names from code
|
|
334
|
+
*/
|
|
335
|
+
function extractEvents(code) {
|
|
336
|
+
const events = new Set();
|
|
337
|
+
const regex = /on([A-Z][a-z]+)/g;
|
|
338
|
+
let match;
|
|
339
|
+
while ((match = regex.exec(code)) !== null) {
|
|
340
|
+
events.add(match[1].toLowerCase());
|
|
341
|
+
}
|
|
342
|
+
return Array.from(events);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Add necessary imports
|
|
346
|
+
*/
|
|
347
|
+
function addImports(code, metadata, options) {
|
|
348
|
+
const imports = [];
|
|
349
|
+
const source = options.importSource || '@fluixi/jsx';
|
|
350
|
+
// Always import jsx basics
|
|
351
|
+
imports.push(`import { jsx, jsxs, Fragment, spread } from '${source}';`);
|
|
352
|
+
// Import signal directive if needed
|
|
353
|
+
if (metadata.hasSignals) {
|
|
354
|
+
imports.push(`import { signal } from '@fluixi/dom/reactive';`);
|
|
355
|
+
}
|
|
356
|
+
// Import control flow if used
|
|
357
|
+
if (code.includes('<Show') ||
|
|
358
|
+
code.includes('<For') ||
|
|
359
|
+
code.includes('<Switch')) {
|
|
360
|
+
imports.push(`import { Show, For, Index, Switch, Match } from '@fluixi/dom/control-flow';`);
|
|
361
|
+
}
|
|
362
|
+
// Import event delegation if used
|
|
363
|
+
if (options.delegateEvents !== false) {
|
|
364
|
+
imports.push(`import { delegateEvents } from '@fluixi/dom/runtime';`);
|
|
365
|
+
}
|
|
366
|
+
return imports.join('\n') + '\n\n' + code;
|
|
367
|
+
}
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Babel Plugin Helpers
|
|
370
|
+
// ============================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Helper for creating a Babel plugin
|
|
373
|
+
*/
|
|
374
|
+
export async function createBabelPlugin(options = {}) {
|
|
375
|
+
// Import the actual Babel plugin using dynamic import for ESM
|
|
376
|
+
let pluginModule = null;
|
|
377
|
+
try {
|
|
378
|
+
// The babel front-end lives in this same package now.
|
|
379
|
+
pluginModule = await import('./frontend/babel/index.js');
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
console.warn('Could not load the babel front-end:', e);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
name: 'babel-fluixi-jsx',
|
|
386
|
+
plugin: pluginModule ? pluginModule.default || pluginModule : null,
|
|
387
|
+
// Provide options mapping
|
|
388
|
+
getOptions() {
|
|
389
|
+
return {
|
|
390
|
+
runtime: 'automatic',
|
|
391
|
+
importSource: options.importSource || '@fluixi/jsx',
|
|
392
|
+
pragma: options.pragma || 'jsx',
|
|
393
|
+
pragmaFrag: options.pragmaFrag || 'Fragment',
|
|
394
|
+
development: options.development || false,
|
|
395
|
+
detectReactivity: true,
|
|
396
|
+
useLitHTML: options.useLitHTML !== false,
|
|
397
|
+
hoistStatics: options.optimize !== false,
|
|
398
|
+
delegateEvents: options.delegateEvents !== false,
|
|
399
|
+
delegatedEvents: options.delegatedEvents || [
|
|
400
|
+
'click',
|
|
401
|
+
'input',
|
|
402
|
+
'change',
|
|
403
|
+
'submit',
|
|
404
|
+
],
|
|
405
|
+
optimizeControlFlow: options.optimize !== false,
|
|
406
|
+
sourceMaps: options.sourceMaps !== false,
|
|
407
|
+
// Codegen backend selection (default 'imperative').
|
|
408
|
+
backend: options.backend || 'imperative',
|
|
409
|
+
...(options.codegen ? { codegen: options.codegen } : {}),
|
|
410
|
+
...(options.signalModule ? { signalModule: options.signalModule } : {}),
|
|
411
|
+
...(options.controlFlowModule
|
|
412
|
+
? { controlFlowModule: options.controlFlowModule }
|
|
413
|
+
: {}),
|
|
414
|
+
...(options.reactiveModule ? { reactiveModule: options.reactiveModule } : {}),
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
// ============================================================================
|
|
420
|
+
// TypeScript Transformer Helpers
|
|
421
|
+
// ============================================================================
|
|
422
|
+
/**
|
|
423
|
+
* Helper for creating a TypeScript transformer
|
|
424
|
+
*/
|
|
425
|
+
export function createTypeScriptTransformer(options = {}) {
|
|
426
|
+
const ts = tryRequire('typescript');
|
|
427
|
+
if (!ts) {
|
|
428
|
+
console.warn('TypeScript not found, transformer unavailable');
|
|
429
|
+
return (context) => (sourceFile) => sourceFile;
|
|
430
|
+
}
|
|
431
|
+
return (context) => {
|
|
432
|
+
const visitor = (node) => {
|
|
433
|
+
// Transform JSX elements
|
|
434
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
435
|
+
// Use the same transformation logic as Babel
|
|
436
|
+
return transformTSJSXNode(node, options, ts);
|
|
437
|
+
}
|
|
438
|
+
return ts.visitEachChild(node, visitor, context);
|
|
439
|
+
};
|
|
440
|
+
return (sourceFile) => {
|
|
441
|
+
return ts.visitNode(sourceFile, visitor);
|
|
442
|
+
};
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Transform TypeScript JSX node
|
|
447
|
+
*/
|
|
448
|
+
function transformTSJSXNode(node, options, ts) {
|
|
449
|
+
// This is a placeholder - full implementation would mirror Babel transformation
|
|
450
|
+
// For now, return the node unchanged
|
|
451
|
+
return node;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Try to require a module, return null if not available
|
|
455
|
+
*/
|
|
456
|
+
function tryRequire(moduleName) {
|
|
457
|
+
try {
|
|
458
|
+
return require(moduleName);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// Vite Plugin
|
|
466
|
+
// ============================================================================
|
|
467
|
+
/**
|
|
468
|
+
* Create a Vite plugin for JSX transformation
|
|
469
|
+
*/
|
|
470
|
+
export function createVitePlugin(options = {}) {
|
|
471
|
+
return {
|
|
472
|
+
name: 'vite-fluixi-jsx',
|
|
473
|
+
enforce: 'pre',
|
|
474
|
+
config() {
|
|
475
|
+
return {
|
|
476
|
+
esbuild: {
|
|
477
|
+
jsx: 'automatic',
|
|
478
|
+
jsxImportSource: options.importSource || '@fluixi/jsx',
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
async transform(code, id) {
|
|
483
|
+
// Load babel plugin dynamically on first transform
|
|
484
|
+
const babelPlugin = await createBabelPlugin(options);
|
|
485
|
+
// Only process JSX/TSX files
|
|
486
|
+
// if (!/\.[jt]sx$/.test(id)) {
|
|
487
|
+
if (!/\.(jsx|tsx|ts)$/i.test(id)) {
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
// console.log('[createVitePlugin] Transforming file:', id);
|
|
491
|
+
try {
|
|
492
|
+
// Use dynamic import instead of require for ESM compatibility
|
|
493
|
+
const babel = await import('@babel/core').catch(() => null);
|
|
494
|
+
if (!babel) {
|
|
495
|
+
// console.log('[createVitePlugin] Babel not found, using fallback');
|
|
496
|
+
// Fallback to simple transform
|
|
497
|
+
return {
|
|
498
|
+
code: transform(code, options).code,
|
|
499
|
+
map: null,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// console.log('[createVitePlugin] Using babel transformation');
|
|
503
|
+
const isTSX = /\.tsx$/.test(id);
|
|
504
|
+
const isTS = /\.ts$/.test(id);
|
|
505
|
+
const presets = [];
|
|
506
|
+
const plugins = [];
|
|
507
|
+
// Add TypeScript preset if needed (but with JSX preservation)
|
|
508
|
+
if (isTSX || isTS) {
|
|
509
|
+
const tsPreset = await import('@babel/preset-typescript')
|
|
510
|
+
.then((m) => m.default || m)
|
|
511
|
+
.catch(() => null);
|
|
512
|
+
if (tsPreset) {
|
|
513
|
+
presets.push([
|
|
514
|
+
tsPreset,
|
|
515
|
+
{
|
|
516
|
+
isTSX: isTSX,
|
|
517
|
+
allExtensions: true,
|
|
518
|
+
// Important: preserve JSX so our plugin can transform it
|
|
519
|
+
jsxPragma: options.pragma || 'jsx',
|
|
520
|
+
jsxPragmaFrag: options.pragmaFrag || 'Fragment',
|
|
521
|
+
onlyRemoveTypeImports: true,
|
|
522
|
+
},
|
|
523
|
+
]);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Only our custom babel plugin handles JSX — never @babel/preset-react, which
|
|
527
|
+
// rewrites spreads with Object.assign and breaks store-proxy reactivity.
|
|
528
|
+
if (babelPlugin && babelPlugin.plugin) {
|
|
529
|
+
plugins.push([babelPlugin.plugin, babelPlugin.getOptions()]);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.warn('[createVitePlugin] custom babel plugin not found');
|
|
533
|
+
}
|
|
534
|
+
const result = await babel.transformAsync(code, {
|
|
535
|
+
filename: id,
|
|
536
|
+
configFile: false,
|
|
537
|
+
babelrc: false,
|
|
538
|
+
presets: presets,
|
|
539
|
+
plugins: plugins,
|
|
540
|
+
sourceMaps: options.sourceMaps !== false,
|
|
541
|
+
});
|
|
542
|
+
// console.log('[createVitePlugin] Transformation complete for:', id);
|
|
543
|
+
// console.log(
|
|
544
|
+
// '[createVitePlugin] Has mergeProps in output:',
|
|
545
|
+
// result?.code?.includes('mergeProps')
|
|
546
|
+
// );
|
|
547
|
+
return {
|
|
548
|
+
code: result?.code || code,
|
|
549
|
+
map: result?.map,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
console.error('Transform error:', e);
|
|
554
|
+
// Return fallback transform on error
|
|
555
|
+
try {
|
|
556
|
+
return {
|
|
557
|
+
code: transform(code, options).code,
|
|
558
|
+
map: null,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
catch (fallbackError) {
|
|
562
|
+
console.error('Fallback transform also failed:', fallbackError);
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// Webpack Loader
|
|
571
|
+
// ============================================================================
|
|
572
|
+
/**
|
|
573
|
+
* Create a Webpack loader for JSX transformation
|
|
574
|
+
*/
|
|
575
|
+
export function createWebpackLoader(options = {}) {
|
|
576
|
+
return async function (source) {
|
|
577
|
+
const callback = this.async();
|
|
578
|
+
const babel = tryRequire('@babel/core');
|
|
579
|
+
const babelPlugin = await createBabelPlugin(options);
|
|
580
|
+
if (!babel || !babelPlugin.plugin) {
|
|
581
|
+
// Fallback to simple transform
|
|
582
|
+
const result = transform(source, options);
|
|
583
|
+
callback(null, result.code);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
babel
|
|
587
|
+
.transformAsync(source, {
|
|
588
|
+
filename: this.resourcePath,
|
|
589
|
+
plugins: [[babelPlugin.plugin, babelPlugin.getOptions()]],
|
|
590
|
+
sourceMaps: options.sourceMaps !== false,
|
|
591
|
+
})
|
|
592
|
+
.then((result) => {
|
|
593
|
+
callback(null, result?.code, result?.map);
|
|
594
|
+
})
|
|
595
|
+
.catch((err) => {
|
|
596
|
+
callback(err);
|
|
597
|
+
});
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
// ============================================================================
|
|
601
|
+
// Rollup Plugin
|
|
602
|
+
// ============================================================================
|
|
603
|
+
/**
|
|
604
|
+
* Create a Rollup plugin for JSX transformation
|
|
605
|
+
*/
|
|
606
|
+
export function createRollupPlugin(options = {}) {
|
|
607
|
+
return {
|
|
608
|
+
name: 'rollup-fluixi-jsx',
|
|
609
|
+
async transform(code, id) {
|
|
610
|
+
const babelPlugin = await createBabelPlugin(options);
|
|
611
|
+
if (!/\.[jt]sx$/.test(id)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const babel = tryRequire('@babel/core');
|
|
616
|
+
if (!babel || !babelPlugin.plugin) {
|
|
617
|
+
const result = transform(code, options);
|
|
618
|
+
return {
|
|
619
|
+
code: result.code,
|
|
620
|
+
map: null,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const result = await babel.transformAsync(code, {
|
|
624
|
+
filename: id,
|
|
625
|
+
plugins: [[babelPlugin.plugin, babelPlugin.getOptions()]],
|
|
626
|
+
sourceMaps: options.sourceMaps !== false,
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
code: result?.code || code,
|
|
630
|
+
map: result?.map,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
catch (e) {
|
|
634
|
+
console.error('Transform error:', e);
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
// ============================================================================
|
|
641
|
+
// ESBuild Plugin
|
|
642
|
+
// ============================================================================
|
|
643
|
+
/**
|
|
644
|
+
* Create an ESBuild plugin for JSX transformation
|
|
645
|
+
*/
|
|
646
|
+
export function createESBuildPlugin(options = {}) {
|
|
647
|
+
return {
|
|
648
|
+
name: 'esbuild-fluixi-jsx',
|
|
649
|
+
setup(build) {
|
|
650
|
+
build.onLoad({ filter: /\.[jt]sx$/ }, async (args) => {
|
|
651
|
+
const babel = tryRequire('@babel/core');
|
|
652
|
+
const babelPlugin = await createBabelPlugin(options);
|
|
653
|
+
const fs = require('fs');
|
|
654
|
+
const source = fs.readFileSync(args.path, 'utf8');
|
|
655
|
+
try {
|
|
656
|
+
if (!babel || !babelPlugin.plugin) {
|
|
657
|
+
const result = transform(source, options);
|
|
658
|
+
return {
|
|
659
|
+
contents: result.code,
|
|
660
|
+
loader: 'js',
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
const result = await babel.transformAsync(source, {
|
|
664
|
+
filename: args.path,
|
|
665
|
+
plugins: [[babelPlugin.plugin, babelPlugin.getOptions()]],
|
|
666
|
+
sourceMaps: false,
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
contents: result?.code || source,
|
|
670
|
+
loader: 'js',
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
catch (e) {
|
|
674
|
+
return {
|
|
675
|
+
errors: [
|
|
676
|
+
{
|
|
677
|
+
text: `Transform error: ${e}`,
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
// ============================================================================
|
|
687
|
+
// Exports
|
|
688
|
+
// ============================================================================
|
|
689
|
+
// ============================================================================
|
|
690
|
+
// "use server" — server functions vite plugin
|
|
691
|
+
// ============================================================================
|
|
692
|
+
export { transformServerFunctions } from './frontend/babel/server-functions.js';
|
|
693
|
+
import { transformServerFunctions as _txServerFns } from './frontend/babel/server-functions.js';
|
|
694
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
695
|
+
import { join as _join } from 'node:path';
|
|
696
|
+
const SERVER_FNS_VMOD = 'virtual:fluixi-server-fns';
|
|
697
|
+
/** Recursively find source files containing the `"use server"` directive under `dir`. */
|
|
698
|
+
function findServerFnModules(dir, out = []) {
|
|
699
|
+
let entries;
|
|
700
|
+
try {
|
|
701
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return out;
|
|
705
|
+
}
|
|
706
|
+
for (const e of entries) {
|
|
707
|
+
if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.'))
|
|
708
|
+
continue;
|
|
709
|
+
const full = _join(dir, e.name);
|
|
710
|
+
if (e.isDirectory())
|
|
711
|
+
findServerFnModules(full, out);
|
|
712
|
+
else if (/\.[jt]sx?$/.test(e.name)) {
|
|
713
|
+
try {
|
|
714
|
+
if (readFileSync(full, 'utf8').includes('use server'))
|
|
715
|
+
out.push(full);
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
/* unreadable — skip */
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Vite plugin for `"use server"`: applies the transform (client→RPC stub, server→register
|
|
726
|
+
* the body), and provides `virtual:fluixi-server-fns` — side-effect imports of every
|
|
727
|
+
* module that defines a server function. The server entry imports it so ALL server
|
|
728
|
+
* functions register at startup (including those in lazy routes, which would otherwise
|
|
729
|
+
* register only when their chunk loads → an RPC registry miss). The client never imports
|
|
730
|
+
* it. `enforce:'pre'` so it runs on the raw source before other transforms.
|
|
731
|
+
*/
|
|
732
|
+
export function serverFunctionsVitePlugin() {
|
|
733
|
+
let root = process.cwd();
|
|
734
|
+
return {
|
|
735
|
+
name: 'fluixi-server-functions',
|
|
736
|
+
enforce: 'pre',
|
|
737
|
+
configResolved(config) {
|
|
738
|
+
root = config.root || root;
|
|
739
|
+
},
|
|
740
|
+
resolveId(id) {
|
|
741
|
+
return id === SERVER_FNS_VMOD ? '\0' + SERVER_FNS_VMOD : null;
|
|
742
|
+
},
|
|
743
|
+
load(id) {
|
|
744
|
+
if (id !== '\0' + SERVER_FNS_VMOD)
|
|
745
|
+
return null;
|
|
746
|
+
const mods = findServerFnModules(_join(root, 'src'));
|
|
747
|
+
// First wire the request-context bridge (server-only), then side-effect import each
|
|
748
|
+
// server-fn module so its top-level $$registerServerFn(...) runs, then re-export the
|
|
749
|
+
// dispatcher so the host can route /_server through THIS module graph (sharing the
|
|
750
|
+
// registry the imports populated) — apps don't need to re-export it from entry-server.
|
|
751
|
+
const setup = `import '@fluixi/start/server-fn-setup';\n`;
|
|
752
|
+
const imports = mods.map((m) => `import ${JSON.stringify(m)};`).join('\n');
|
|
753
|
+
const dispatcher = `\nexport { isServerFnRequest, handleServerFn } from '@fluixi/start/server-fn';\n`;
|
|
754
|
+
return setup + imports + dispatcher;
|
|
755
|
+
},
|
|
756
|
+
transform(code, id, opts) {
|
|
757
|
+
if (!/\.[jt]sx?$/.test(id))
|
|
758
|
+
return null;
|
|
759
|
+
if (!code.includes('use server'))
|
|
760
|
+
return null;
|
|
761
|
+
return _txServerFns(code, { ssr: !!opts?.ssr, filename: id, root });
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
export default {
|
|
766
|
+
// Analysis
|
|
767
|
+
analyzeProp,
|
|
768
|
+
analyzeProps,
|
|
769
|
+
shouldUseLitTemplate,
|
|
770
|
+
isLitTemplate,
|
|
771
|
+
// Code generation
|
|
772
|
+
generateCreateElement,
|
|
773
|
+
generatePropsCode,
|
|
774
|
+
generateChildrenCode,
|
|
775
|
+
generateLitHTMLTemplate,
|
|
776
|
+
// Optimization
|
|
777
|
+
optimizeStatic,
|
|
778
|
+
optimizeSpreads,
|
|
779
|
+
hoistStatic,
|
|
780
|
+
generateEventDelegation,
|
|
781
|
+
// Transform
|
|
782
|
+
transform,
|
|
783
|
+
// Plugin creators
|
|
784
|
+
createBabelPlugin,
|
|
785
|
+
createTypeScriptTransformer,
|
|
786
|
+
createVitePlugin,
|
|
787
|
+
createWebpackLoader,
|
|
788
|
+
createRollupPlugin,
|
|
789
|
+
createESBuildPlugin,
|
|
790
|
+
};
|