@fluixi/compiler 1.0.0-alpha.73 → 1.0.0-alpha.74

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.
Files changed (109) hide show
  1. package/README.md +41 -16
  2. package/dist/analyze/expr-shape.cjs +1 -0
  3. package/dist/analyze/expr-shape.d.ts +15 -0
  4. package/dist/analyze/expr-shape.d.ts.map +1 -0
  5. package/dist/analyze/expr-shape.js +37 -0
  6. package/dist/analyze/expr-shape.mjs +1 -0
  7. package/dist/analyze/reactive-expr.cjs +1 -0
  8. package/dist/analyze/reactive-expr.d.ts +39 -0
  9. package/dist/analyze/reactive-expr.d.ts.map +1 -0
  10. package/dist/analyze/reactive-expr.js +31 -0
  11. package/dist/analyze/reactive-expr.mjs +1 -0
  12. package/dist/babel-GXO3KAVI.mjs +1 -0
  13. package/dist/chunk-3SAEGOMQ.mjs +1 -0
  14. package/dist/chunk-E2PW2OZU.mjs +1 -0
  15. package/dist/chunk-IBRM4W7I.mjs +10 -0
  16. package/dist/chunk-OHHZCYIQ.mjs +1 -0
  17. package/dist/chunk-TIE4XNAM.mjs +1 -0
  18. package/dist/frontend/babel/build-ir-lit.cjs +1 -1
  19. package/dist/frontend/babel/build-ir-lit.d.ts +1 -0
  20. package/dist/frontend/babel/build-ir-lit.d.ts.map +1 -1
  21. package/dist/frontend/babel/build-ir-lit.mjs +1 -1
  22. package/dist/frontend/babel/build-ir.cjs +1 -1
  23. package/dist/frontend/babel/build-ir.d.ts +13 -11
  24. package/dist/frontend/babel/build-ir.d.ts.map +1 -1
  25. package/dist/frontend/babel/build-ir.js +27 -217
  26. package/dist/frontend/babel/build-ir.mjs +1 -1
  27. package/dist/frontend/babel/index.cjs +1 -2
  28. package/dist/frontend/babel/index.mjs +1 -2
  29. package/dist/frontend/babel/lower-template.cjs +1 -1
  30. package/dist/frontend/babel/lower-template.d.ts +12 -9
  31. package/dist/frontend/babel/lower-template.d.ts.map +1 -1
  32. package/dist/frontend/babel/lower-template.js +33 -445
  33. package/dist/frontend/babel/lower-template.mjs +1 -1
  34. package/dist/frontend/babel/plugin.cjs +1 -2
  35. package/dist/frontend/babel/plugin.d.ts +24 -85
  36. package/dist/frontend/babel/plugin.d.ts.map +1 -1
  37. package/dist/frontend/babel/plugin.js +52 -1285
  38. package/dist/frontend/babel/plugin.mjs +1 -2
  39. package/dist/frontend/babel/server-functions.cjs +1 -1
  40. package/dist/frontend/babel/server-functions.d.ts.map +1 -1
  41. package/dist/frontend/babel/server-functions.js +2 -9
  42. package/dist/frontend/babel/server-functions.mjs +1 -1
  43. package/dist/index.cjs +8 -8
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +8 -0
  47. package/dist/index.mjs +9 -9
  48. package/dist/integrations.cjs +15 -19
  49. package/dist/integrations.d.ts +39 -151
  50. package/dist/integrations.d.ts.map +1 -1
  51. package/dist/integrations.js +79 -491
  52. package/dist/integrations.mjs +11 -17
  53. package/dist/lower/compile-template.cjs +1 -0
  54. package/dist/lower/compile-template.d.ts +57 -0
  55. package/dist/lower/compile-template.d.ts.map +1 -0
  56. package/dist/lower/compile-template.js +72 -0
  57. package/dist/lower/compile-template.mjs +1 -0
  58. package/dist/lower/jsx.cjs +1 -0
  59. package/dist/lower/jsx.d.ts +24 -0
  60. package/dist/lower/jsx.d.ts.map +1 -0
  61. package/dist/lower/jsx.js +193 -0
  62. package/dist/lower/jsx.mjs +1 -0
  63. package/dist/lower/template.cjs +1 -0
  64. package/dist/lower/template.d.ts +41 -0
  65. package/dist/lower/template.d.ts.map +1 -0
  66. package/dist/lower/template.js +445 -0
  67. package/dist/lower/template.mjs +1 -0
  68. package/dist/lower/text.cjs +1 -0
  69. package/dist/lower/text.d.ts +11 -0
  70. package/dist/lower/text.d.ts.map +1 -0
  71. package/dist/lower/text.js +31 -0
  72. package/dist/lower/text.mjs +1 -0
  73. package/dist/options.cjs +1 -1
  74. package/dist/options.d.ts +0 -2
  75. package/dist/options.d.ts.map +1 -1
  76. package/dist/options.js +0 -1
  77. package/dist/options.mjs +1 -1
  78. package/dist/server-fns-6QM243HC.mjs +2 -0
  79. package/dist/server-functions-E3LKCZ4R.mjs +1 -0
  80. package/dist/transform/bindings.cjs +1 -0
  81. package/dist/transform/bindings.d.ts +17 -0
  82. package/dist/transform/bindings.d.ts.map +1 -0
  83. package/dist/transform/bindings.js +69 -0
  84. package/dist/transform/bindings.mjs +1 -0
  85. package/dist/transform/index.cjs +3 -0
  86. package/dist/transform/index.d.ts +12 -0
  87. package/dist/transform/index.d.ts.map +1 -0
  88. package/dist/transform/index.js +10 -0
  89. package/dist/transform/index.mjs +12 -0
  90. package/dist/transform/server-fn-id.cjs +1 -0
  91. package/dist/transform/server-fn-id.d.ts +10 -0
  92. package/dist/transform/server-fn-id.d.ts.map +1 -0
  93. package/dist/transform/server-fn-id.js +18 -0
  94. package/dist/transform/server-fn-id.mjs +1 -0
  95. package/dist/transform/server-fns.cjs +2 -0
  96. package/dist/transform/server-fns.d.ts +13 -0
  97. package/dist/transform/server-fns.d.ts.map +1 -0
  98. package/dist/transform/server-fns.js +94 -0
  99. package/dist/transform/server-fns.mjs +11 -0
  100. package/dist/transform/templates.cjs +3 -0
  101. package/dist/transform/templates.d.ts +26 -0
  102. package/dist/transform/templates.d.ts.map +1 -0
  103. package/dist/transform/templates.js +347 -0
  104. package/dist/transform/templates.mjs +12 -0
  105. package/dist/transform-PJX2GNSV.mjs +3 -0
  106. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  107. package/package.json +29 -8
  108. package/dist/babel-E7EQLX36.mjs +0 -2
  109. package/dist/chunk-EF6V4GTD.mjs +0 -1
@@ -1,85 +1,44 @@
1
1
  /**
2
- * @fileoverview Comprehensive Babel Plugin for Reactive JSX Transformation
2
+ * @fileoverview Babel front-end for the Fluixi compiler.
3
3
  * @module @fluixi/babel-plugin-jsx
4
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
5
+ * This is a host adapter, not the compiler. It parses with babel and, for each
6
+ * JSX element/fragment and each `` html`...` `` template, hands the node to the
7
+ * shared pipeline — build IR, run the imperative backend — then splices the
8
+ * emitted code back into the program. Everything that decides *what* the output
9
+ * means (directives, control flow, component resolution, load strategies,
10
+ * reactivity, templates) lives in ../../{ir,resolve,optimize,codegen}, which
11
+ * import no babel at all.
12
+ *
13
+ * What stays here is genuinely host work: dispatching the visitors, collecting
14
+ * the runtime symbols the backend reported so the imports can be injected, and
15
+ * hoisting static templates to module scope.
12
16
  */
13
17
  import { declare } from '@babel/helper-plugin-utils';
14
18
  import { createComponentResolver, } from '../../resolve/component-resolver.js';
15
19
  import { resolveComponentSources } from '../../resolve/resolve-ir.js';
16
20
  import { collapseStrategies } from '../../optimize/collapse-strategies.js';
17
- import { EAGER, isDeferred, isLoadDirective, parseLoadDirective, LoadDirectiveError, } from '../../resolve/load-directive.js';
18
- import { DOM_CONTROL_FLOW, CORE_CONTROL_FLOW, ROUTER_COMPONENTS as ROUTER_COMPONENT_NAMES, } from '../../resolve/builtins.js';
19
- import { types as t, template, } from '@babel/core';
21
+ import { isDeferred } from '../../resolve/load-directive.js';
22
+ import { CORE_CONTROL_FLOW, ROUTER_COMPONENTS as ROUTER_COMPONENT_NAMES, } from '../../resolve/builtins.js';
23
+ import { types as t, template } from '@babel/core';
20
24
  import { buildIR } from './build-ir.js';
21
25
  import { buildIRFromLit } from './build-ir-lit.js';
22
26
  import { imperativeBackend } from '../../codegen/backends/imperative.js';
23
27
  // ============================================================================
24
28
  // Constants
25
29
  // ============================================================================
26
- // The names come from `resolve/builtins.ts`, which publishes them for the TS plugin,
27
- // the language server and codemods — tools that need to know where `<Show>` comes from
28
- // but must not re-derive it. What stays here is the part that is genuinely codegen:
29
- // which names get special lowering, reactive-getter props, and memo exemption.
30
- const CONTROL_FLOW_COMPONENTS = new Set([...DOM_CONTROL_FLOW, ...CORE_CONTROL_FLOW]);
31
30
  // Control-flow components that live in @fluixi/core, NOT @fluixi/dom — they must
32
31
  // be auto-imported from core (dom doesn't export them).
33
32
  const CORE_ONLY_CONTROL_FLOW = new Set(CORE_CONTROL_FLOW);
34
33
  // Router components auto-imported (from routerModule) the same way control flow is:
35
34
  // use `<Router>`/`<Outlet>`/… in a template and the import is injected if absent.
36
35
  const ROUTER_COMPONENTS = new Set(ROUTER_COMPONENT_NAMES);
37
- // Control-flow components read a single reactive *value* prop inside their own tracking
38
- // scope. Emit that prop as a getter (like `children`) so reading it re-runs the caller's
39
- // expression — otherwise `each={items()}` / `when={cond()}` snapshot once and never update.
40
- // The runtime routes these through `resolveReactiveProp`, which also unwraps the accessor
41
- // form (`each={items}`), so all three shapes work with no compiler-side ambiguity.
42
- //
43
- // Excluded on purpose: `Dynamic.component` (a component IS a function, so the value can't be
44
- // told apart from an accessor — its idiomatic `component={signal}` form already reacts) and
45
- // `Portal.mount` (read once at mount, not a reactive value).
46
- const CONTROL_FLOW_VALUE_PROPS = {
47
- Show: 'when',
48
- For: 'each',
49
- Index: 'each',
50
- Match: 'when',
51
- };
52
- const REACTIVE_PATTERNS = {
53
- signalCall: /\w+\(\)$/,
54
- storeAccess: /\w+\.\w+/,
55
- arrowFunction: /^\(\)\s*=>/,
56
- functionCall: /^function\s*\(/,
57
- };
58
- const EVENT_PROPS = /^on[A-Z]/;
59
- const DELEGATABLE_EVENTS = [
60
- 'click',
61
- 'dblclick',
62
- 'input',
63
- 'change',
64
- 'submit',
65
- 'focus',
66
- 'blur',
67
- 'keydown',
68
- 'keyup',
69
- 'keypress',
70
- 'mousedown',
71
- 'mouseup',
72
- 'mouseover',
73
- 'mouseout',
74
- 'mouseenter',
75
- 'mouseleave',
76
- ];
77
36
  // ============================================================================
78
37
  // Main Plugin
79
38
  // ============================================================================
80
39
  export default declare((api, options = {}) => {
81
40
  api.assertVersion(7);
82
- 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', routerModule = '@fluixi/core/router-next', reactiveModule = '@fluixi/reactive/signal', autoShowTransform = false, backend = 'imperative', codegen = 'ir', templateClone = true, partialTemplates = true,
41
+ const { delegateEvents = true, signalModule = '@fluixi/dom', controlFlowModule = '@fluixi/dom', routerModule = '@fluixi/core/router-next', reactiveModule = '@fluixi/reactive/signal', templateClone = true, partialTemplates = true,
83
42
  // `both` by default: JSX and html`` are compiled out of the box, so
84
43
  // `fluixi()` needs no `format` option. Injected imports are deduped, so a
85
44
  // file mixing the two styles won't redeclare a binding.
@@ -89,8 +48,6 @@ export default declare((api, options = {}) => {
89
48
  const componentResolver = createComponentResolver(resolveRules);
90
49
  const compileLit = format === 'lit' || format === 'both';
91
50
  const pluginState = {
92
- backend,
93
- codegen,
94
51
  irImports: new Set(),
95
52
  libModule: '@fluixi/core',
96
53
  controlFlowModule,
@@ -98,9 +55,7 @@ export default declare((api, options = {}) => {
98
55
  templateClone,
99
56
  partialTemplates,
100
57
  hasJSX: false,
101
- hasReactivity: false,
102
58
  hasLitHTML: false,
103
- needsSignalImport: false,
104
59
  needsControlFlowImport: false,
105
60
  needsCreateMemo: false,
106
61
  controlFlowComponents: new Set(),
@@ -108,7 +63,6 @@ export default declare((api, options = {}) => {
108
63
  deferredComponents: new Map(),
109
64
  componentResolver,
110
65
  delegatedEvents: new Set(),
111
- staticElements: new Map(),
112
66
  staticTemplates: new Map(),
113
67
  staticCounter: 0,
114
68
  };
@@ -138,10 +92,8 @@ export default declare((api, options = {}) => {
138
92
  // Reset state for new file
139
93
  Object.assign(state, {
140
94
  hasJSX: false,
141
- hasReactivity: false,
142
95
  hasLitHTML: false,
143
96
  hasLitIR: false,
144
- needsSignalImport: false,
145
97
  needsControlFlowImport: false,
146
98
  needsCreateMemo: false,
147
99
  controlFlowComponents: new Set(),
@@ -149,17 +101,13 @@ export default declare((api, options = {}) => {
149
101
  deferredComponents: new Map(),
150
102
  componentResolver,
151
103
  delegatedEvents: new Set(),
152
- staticElements: new Map(),
153
104
  staticTemplates: new Map(),
154
- autoShow: autoShowTransform,
155
105
  staticCounter: 0,
156
106
  libModule: '@fluixi/core',
157
107
  controlFlowModule,
158
108
  routerModule,
159
109
  templateClone,
160
110
  partialTemplates,
161
- backend,
162
- codegen,
163
111
  irImports: new Set(),
164
112
  needsCreateNativeElement: false,
165
113
  needsInsert: false,
@@ -170,19 +118,10 @@ export default declare((api, options = {}) => {
170
118
  },
171
119
  exit(path, state) {
172
120
  const imports = [];
173
- if (!state.hasJSX) {
174
- // Add signal directive import if needed
175
- if (state.needsSignalImport) {
176
- imports.push(t.importDeclaration([
177
- t.importSpecifier(t.identifier('signal'), t.identifier('signal')),
178
- ], t.stringLiteral(signalModule)));
179
- }
180
- }
181
121
  if (state.hasJSX || state.hasLitIR) {
182
122
  if (state.irImports.size > 0) {
183
- // IR pipeline (JSX codegen:'ir' and/or lit templates): import exactly
184
- // the symbols emit() reported, splitting control-flow components to
185
- // the control-flow module.
123
+ // Import exactly the symbols emit() reported, splitting control-flow
124
+ // components to the control-flow module.
186
125
  const CF = new Set([
187
126
  'Show', 'For', 'Index', 'Switch', 'Match',
188
127
  'Dynamic', 'ErrorBoundary', 'Portal', 'Suspense',
@@ -208,48 +147,10 @@ export default declare((api, options = {}) => {
208
147
  imports.push(t.importDeclaration(cf.map(spec), t.stringLiteral(controlFlowModule)));
209
148
  }
210
149
  }
211
- if (state.hasJSX && backend === 'imperative' && codegen !== 'ir') {
212
- // Compiled imperative output: import only the runtime primitives
213
- // actually emitted, straight from the dom runtime module.
214
- const want = [
215
- [!!state.needsCreateNativeElement, 'createNativeElement'],
216
- [!!state.needsSpread, 'spread'],
217
- [!!state.needsInsert, 'insert'],
218
- [!!state.needsCreateComponent, 'createComponent'],
219
- [!!state.needsMergeProps, 'mergeProps'],
220
- ];
221
- const specifiers = want
222
- .filter(([need, name]) => need && !path.scope.hasBinding(name))
223
- .map(([, name]) => t.importSpecifier(t.identifier(name), t.identifier(name)));
224
- if (specifiers.length > 0) {
225
- imports.push(t.importDeclaration(specifiers, t.stringLiteral(signalModule)));
226
- }
227
- }
228
- else if (state.hasJSX && runtime === 'automatic' && backend !== 'imperative') {
229
- // Not under the imperative backend: `codegen: 'ir'` lands here because the
230
- // branch above excludes it, and emitted an unused jsx/jsxs/Fragment import.
231
- // The IR path produces runtime calls, never JSX factory calls.
232
- // Add JSX runtime imports
233
- const specifiers = [
234
- t.importSpecifier(t.identifier('jsx'), t.identifier('jsx')),
235
- t.importSpecifier(t.identifier('jsxs'), t.identifier('jsxs')),
236
- t.importSpecifier(t.identifier('Fragment'), t.identifier('Fragment')),
237
- ];
238
- if (development) {
239
- specifiers.push(t.importSpecifier(t.identifier('jsxDEV'), t.identifier('jsxDEV')));
240
- }
241
- // Add mergeProps import if spreads are used
242
- if (state.needsMergeProps) {
243
- specifiers.push(t.importSpecifier(t.identifier('mergeProps'), t.identifier('mergeProps')));
244
- }
245
- imports.push(t.importDeclaration(specifiers, t.stringLiteral(importSource)));
246
- }
247
150
  // createMemo (wraps conditionals/components) is a reactive primitive,
248
151
  // so it comes from the reactive module, not dom.
249
152
  if (state.needsCreateMemo && !path.scope.hasBinding('createMemo')) {
250
- imports.push(t.importDeclaration([t.importSpecifier(t.identifier('createMemo'), t.identifier('createMemo'))], t.stringLiteral(backend === 'imperative'
251
- ? reactiveModule
252
- : state.libModule ?? '@fluixi/core')));
153
+ imports.push(t.importDeclaration([t.importSpecifier(t.identifier('createMemo'), t.identifier('createMemo'))], t.stringLiteral(reactiveModule)));
253
154
  }
254
155
  // Add control flow imports for auto-transformed components (Show, etc.)
255
156
  // Only add components that aren't already bound in the file scope.
@@ -320,23 +221,13 @@ export default declare((api, options = {}) => {
320
221
  if (delegateEvents && state.delegatedEvents.size > 0) {
321
222
  imports.push(t.importDeclaration([
322
223
  t.importSpecifier(t.identifier('delegateEvents'), t.identifier('delegateEvents')),
323
- ], t.stringLiteral(backend === 'imperative' ? signalModule : '@fluixi/jsx')));
224
+ ], t.stringLiteral(signalModule)));
324
225
  // Add delegateEvents call at top of program
325
226
  const eventsArray = t.arrayExpression(Array.from(state.delegatedEvents).map((event) => t.stringLiteral(event)));
326
227
  const delegateCall = t.expressionStatement(t.callExpression(t.identifier('delegateEvents'), [eventsArray]));
327
228
  path.node.body.unshift(delegateCall);
328
229
  }
329
- // Add hoisted static elements
330
- if (hoistStatics && state.staticElements.size > 0) {
331
- const hoisted = [];
332
- state.staticElements.forEach((value, key) => {
333
- hoisted.push(t.variableDeclaration('const', [
334
- t.variableDeclarator(t.identifier(key), value),
335
- ]));
336
- });
337
- path.node.body.unshift(...hoisted);
338
- }
339
- // After the hoisted statics but before imports are unshifted, so the
230
+ // Before imports are unshifted, so the
340
231
  // final order is: imports, deferred bindings, everything else.
341
232
  if (deferredDecls.length > 0) {
342
233
  path.node.body.unshift(...deferredDecls);
@@ -368,109 +259,24 @@ export default declare((api, options = {}) => {
368
259
  }
369
260
  },
370
261
  },
262
+ // Both JSX forms take the one path: build IR, run the imperative backend,
263
+ // splice the emitted code back in. It handles the whole subtree, so nothing
264
+ // below this node needs its own visitor.
371
265
  JSXElement(path, state) {
372
266
  state.hasJSX = true;
373
- // IR pipeline path: build IR -> emit() -> reparse. Handles the whole
374
- // subtree, so we bypass the inline transform entirely.
375
- if (backend === 'imperative' && codegen === 'ir') {
376
- path.replaceWith(emitViaIR(path.node, state));
377
- return;
378
- }
379
- const element = path.node;
380
- const openingElement = element.openingElement;
381
- // Router components auto-import like control flow, but aren't optimized —
382
- // just register the name so the import gets injected if it isn't bound. A
383
- // configured rule takes precedence; registering anyway would inject the
384
- // router's import alongside the resolved one and bind the name twice.
385
- if (t.isJSXIdentifier(openingElement.name) &&
386
- ROUTER_COMPONENTS.has(openingElement.name.name) &&
387
- !state.componentResolver?.resolve(openingElement.name.name)) {
388
- state.controlFlowComponents.add(openingElement.name.name);
389
- state.needsControlFlowImport = true;
390
- }
391
- // Check if it's a control flow component
392
- if (t.isJSXIdentifier(openingElement.name) &&
393
- CONTROL_FLOW_COMPONENTS.has(openingElement.name.name)) {
394
- state.controlFlowComponents.add(openingElement.name.name);
395
- if (optimizeControlFlow) {
396
- const optimized = optimizeControlFlowComponent(element, state);
397
- if (optimized) {
398
- path.replaceWith(optimized);
399
- return;
400
- }
401
- }
402
- }
403
- // Try to hoist static elements.
404
- // Not in imperative mode: hoisting a live DOM node would share one node
405
- // across renders. Static hoisting there needs template-clone (contract v2).
406
- if (hoistStatics && backend !== 'imperative' && isStaticElement(element, state)) {
407
- const hoisted = hoistStaticElement(element, state);
408
- if (hoisted) {
409
- path.replaceWith(hoisted);
410
- return;
411
- }
412
- }
413
- // Transform to JSX runtime calls
414
- const transformed = transformJSXElement(element, state, {
415
- runtime,
416
- pragma,
417
- development,
418
- detectReactivity,
419
- useLitHTML,
420
- autoShow: autoShowTransform,
421
- });
422
- path.replaceWith(transformed);
267
+ path.replaceWith(emitViaIR(path.node, state));
423
268
  },
424
269
  JSXFragment(path, state) {
425
270
  state.hasJSX = true;
426
- if (backend === 'imperative' && codegen === 'ir') {
427
- path.replaceWith(emitViaIR(path.node, state));
428
- return;
429
- }
430
- const transformed = transformJSXFragment(path.node, state, {
431
- runtime,
432
- pragmaFrag,
433
- development,
434
- });
435
- path.replaceWith(transformed);
436
- },
437
- // Detect reactive patterns in code
438
- CallExpression(path, state) {
439
- if (!detectReactivity)
440
- return;
441
- const callee = path.node.callee;
442
- if (callee.name === 'handleToggle') {
443
- // console.log('call express', path);
444
- }
445
- // Detect signal creation
446
- if (t.isIdentifier(callee) &&
447
- (callee.name === 'createSignal' ||
448
- callee.name === 'createStore' ||
449
- callee.name === 'useContext' ||
450
- callee.name === 'useLocation')) {
451
- // console.log('call express', callee.name);
452
- state.hasReactivity = true;
453
- }
454
- },
455
- // NOTE: JSXExpressionContainer visitor is intentionally a no-op.
456
- // JSXElement.enter fires BEFORE children are traversed, so by the time
457
- // Babel would visit nested JSXExpressionContainers the JSXElement has
458
- // already been replaced via path.replaceWith(). Attribute and children
459
- // expressions are processed directly in transformAttributeValue /
460
- // transformChildren above.
461
- JSXExpressionContainer(_path) {
462
- // no-op
271
+ path.replaceWith(emitViaIR(path.node, state));
463
272
  },
464
273
  TaggedTemplateExpression(path, state) {
465
- // Compile `html`...`` (and `svg`...``) templates through the shared IR
466
- // pipeline: same IR the JSX front-end builds, same imperative backend,
467
- // same runtime calls.
274
+ // `html`...`` and `svg`...`` lower through the same IR and the same
275
+ // backend as JSX one pipeline, two authoring syntaxes.
468
276
  if (compileLit &&
469
- backend === 'imperative' &&
470
277
  t.isIdentifier(path.node.tag) &&
471
278
  (path.node.tag.name === litTag || path.node.tag.name === 'svg')) {
472
279
  path.replaceWith(emitLitViaIR(path.node, state));
473
- return;
474
280
  }
475
281
  },
476
282
  },
@@ -479,128 +285,34 @@ export default declare((api, options = {}) => {
479
285
  // ============================================================================
480
286
  // JSX Transformation
481
287
  // ============================================================================
482
- function transformJSXElement(element, state, options) {
483
- const { runtime, pragma, development, autoShow = true } = options;
484
- if (runtime === 'automatic') {
485
- return transformAutomaticElement(element, state, development, autoShow);
486
- }
487
- else {
488
- return transformClassicElement(element, state, pragma);
489
- }
490
- }
491
- // ============================================================================
492
- // Imperative backend emission
493
- //
494
- // Emits direct calls into the @fluixi/dom runtime instead of jsx()/jsxs():
495
- // native element -> (() => { const _el = createNativeElement(tag);
496
- // spread({ element: _el, props });
497
- // insert(_el, children);
498
- // return _el; })()
499
- // component -> createMemo(() => createComponent(Comp, props))
500
- // This matches @fluixi/jsx's component semantics exactly while removing
501
- // the per-render jsx() dispatch for host elements. Targets RUNTIME_CONTRACT_V1.
502
- // ============================================================================
503
- // Build-time SVG detection, kept in lockstep with isSVGElement in the dom runtime.
504
- const SVG_TAGS = new Set([
505
- 'svg', 'path', 'circle', 'rect', 'line', 'polygon', 'polyline',
506
- 'ellipse', 'g', 'defs', 'clipPath', 'text',
507
- ]);
508
- /** Build the props object, merging spreads via mergeProps (reactivity-preserving). */
509
- function buildPropsObject(props, spreads, state) {
510
- if (spreads.length > 0) {
511
- state.needsMergeProps = true;
512
- return t.callExpression(t.identifier('mergeProps'), [
513
- ...spreads,
514
- t.objectExpression(props),
515
- ]);
516
- }
517
- return t.objectExpression(props);
518
- }
519
- /** Wrap children expressions into a single value for `insert` / `children` prop. */
520
- function childrenValue(children) {
521
- return children.length === 1 ? children[0] : t.arrayExpression(children);
522
- }
523
- function emitImperativeElement(tag, isNative, props, spreads, children, state) {
524
- if (!isNative) {
525
- // Component: children go into props as a LAZY getter, so child components are
526
- // created inside the component's execution (owner nesting for context/Suspense),
527
- // after the component runs (provider ordering), and lazily (ErrorBoundary catch).
528
- const allProps = [...props];
529
- if (children.length > 0) {
530
- allProps.push(t.objectMethod('get', t.identifier('children'), [], t.blockStatement([t.returnStatement(childrenValue(children))])));
531
- }
532
- return emitComponentCall(tag, buildPropsObject(allProps, spreads, state), state);
533
- }
534
- // Native element: createNativeElement + spread (attrs only) + insert (children).
535
- const tagName = tag.value;
536
- const isSVG = SVG_TAGS.has(tagName);
537
- const elId = t.identifier('_el$');
538
- const stmts = [];
539
- state.needsCreateNativeElement = true;
540
- const createArgs = [tag];
541
- if (isSVG)
542
- createArgs.push(t.booleanLiteral(true));
543
- stmts.push(t.variableDeclaration('const', [
544
- t.variableDeclarator(elId, t.callExpression(t.identifier('createNativeElement'), createArgs)),
545
- ]));
546
- if (props.length > 0 || spreads.length > 0) {
547
- state.needsSpread = true;
548
- const spreadProps = [
549
- t.objectProperty(t.identifier('element'), elId),
550
- t.objectProperty(t.identifier('props'), buildPropsObject(props, spreads, state)),
551
- ];
552
- if (isSVG) {
553
- spreadProps.push(t.objectProperty(t.identifier('isSVG'), t.booleanLiteral(true)));
554
- }
555
- stmts.push(t.expressionStatement(t.callExpression(t.identifier('spread'), [t.objectExpression(spreadProps)])));
556
- }
557
- // Insert each child individually (like the jsx runtime's applyProps), so a
558
- // reactive/component child that isn't the only child still gets its own
559
- // reactive binding. A single insert(el, [array]) does NOT bind function
560
- // elements sitting inside the array.
561
- if (children.length > 0) {
562
- state.needsInsert = true;
563
- for (const child of children) {
564
- const isFn = t.isArrowFunctionExpression(child) ||
565
- t.isFunctionExpression(child) ||
566
- (t.isCallExpression(child) &&
567
- t.isIdentifier(child.callee) &&
568
- child.callee.name === 'createMemo');
569
- const args = isFn
570
- ? [t.cloneNode(elId), child, t.nullLiteral()]
571
- : [t.cloneNode(elId), child];
572
- stmts.push(t.expressionStatement(t.callExpression(t.identifier('insert'), args)));
573
- }
574
- }
575
- stmts.push(t.returnStatement(elId));
576
- return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(stmts)), []);
577
- }
578
- /**
579
- * Emit a component invocation. jsx backend: `jsx(Comp, props)`. Imperative:
580
- * `createMemo(() => createComponent(Comp, props))` — same shape jsx-runtime
581
- * produces for components, so it stays reactive when used as a child.
582
- */
583
- /**
584
- * Read a `load:` attribute's literal value. A `${…}` hole is rejected: the strategy
585
- * decides how the module is emitted, so it has to be known at compile time.
586
- */
587
- function readLoadDirective(name, attr) {
588
- let value = null;
589
- if (attr.value) {
590
- if (t.isStringLiteral(attr.value))
591
- value = attr.value.value;
592
- else {
593
- throw new LoadDirectiveError(`'${name}' needs a literal value, not an expression — the strategy is compile-time.`);
594
- }
595
- }
596
- return parseLoadDirective(name, value);
597
- }
598
288
  /**
599
289
  * Two usages of one component in the same file share a binding, so their strategies
600
290
  * have to agree. The earlier-loading one wins: the module is in whichever chunk loads
601
291
  * first regardless, so deferring the other buys nothing — the same reasoning as the
602
292
  * collapse pass, applied within a file.
603
293
  */
294
+ /**
295
+ * What to do with JSX or a template found inside an expression: resolve its
296
+ * component references and record what the module needs, exactly as the
297
+ * top-level node gets. The IR itself is emitted and dropped by the walk, so
298
+ * anything not taken off it here is lost — which is how `{(b) => <Input/>}`
299
+ * came out referring to a component nothing imported.
300
+ */
301
+ function nestedHook(state) {
302
+ return (ir) => {
303
+ resolveComponentSources(ir, {
304
+ resolver: state.componentResolver,
305
+ modules: {
306
+ controlFlowModule: state.controlFlowModule,
307
+ coreModule: state.libModule,
308
+ routerModule: state.routerModule,
309
+ },
310
+ strategyFor: (node) => node.load,
311
+ });
312
+ collapseStrategies(ir);
313
+ collectRuntimeNeeds(ir, state);
314
+ };
315
+ }
604
316
  /** The strategy as an object literal the runtime can read. */
605
317
  function strategyLiteral(strategy) {
606
318
  const props = [
@@ -617,84 +329,6 @@ function strategyLiteral(strategy) {
617
329
  }
618
330
  return t.objectExpression(props);
619
331
  }
620
- function mergeStrategy(existing, incoming) {
621
- if (!isDeferred(existing) || !isDeferred(incoming))
622
- return EAGER;
623
- return existing;
624
- }
625
- function emitComponentCall(comp, propsObj, state) {
626
- // Configured tags register here rather than in the JSX element visitor: this is the
627
- // chokepoint every component passes through, and the visitor missed children because
628
- // a lowered parent is replaced before its subtree is walked.
629
- //
630
- // A configured rule wins over the router built-ins. Those are only an import — no
631
- // special lowering, no reactive-getter props — so letting a project's own `Link` lose
632
- // to them silently was the same failure `Switch` has: the tag renders the router's
633
- // component and nothing reports it. Control flow still wins, because there the name
634
- // selects codegen and a rule cannot supply that.
635
- // Control flow needs its import injected too, and registering it here rather than in
636
- // the JSX visitor is what makes nested usage work: a lowered parent is replaced before
637
- // its subtree is walked, so `<div><Show/></div>` never reached the visitor and `Show`
638
- // compiled to a reference to nothing. Every fixture imported Show by hand, so nothing
639
- // caught it.
640
- if (t.isIdentifier(comp) && CONTROL_FLOW_COMPONENTS.has(comp.name)) {
641
- state.controlFlowComponents.add(comp.name);
642
- state.needsControlFlowImport = true;
643
- }
644
- let resolvedByRule = false;
645
- if (t.isIdentifier(comp) && !CONTROL_FLOW_COMPONENTS.has(comp.name) && state.componentResolver) {
646
- const resolved = state.componentResolver.resolve(comp.name);
647
- if (resolved) {
648
- const strategy = state.pendingLoad ?? EAGER;
649
- // An eager usage seen earlier in the file already put the module in the chunk,
650
- // so a later deferred usage of the same name collapses to eager. Without this the
651
- // file gets both a static import and a `const` for one name — a redeclaration.
652
- const alreadyEager = state.resolvedComponents.has(comp.name);
653
- if (isDeferred(strategy) && !alreadyEager) {
654
- // A deferred component becomes a `deferred(() => import(...))` binding rather
655
- // than a static import, so the bundler puts it in its own chunk.
656
- const existing = state.deferredComponents.get(comp.name);
657
- const merged = existing ? mergeStrategy(existing.strategy, strategy) : strategy;
658
- if (isDeferred(merged)) {
659
- state.deferredComponents.set(comp.name, { ...resolved, strategy: merged });
660
- }
661
- else {
662
- state.deferredComponents.delete(comp.name);
663
- state.resolvedComponents.set(comp.name, resolved);
664
- }
665
- }
666
- else {
667
- // An eager usage anywhere wins: the module is in the chunk already.
668
- state.deferredComponents.delete(comp.name);
669
- state.resolvedComponents.set(comp.name, resolved);
670
- }
671
- resolvedByRule = true;
672
- }
673
- }
674
- state.pendingLoad = undefined;
675
- // Router components auto-import like control flow — register the name here so
676
- // `<Outlet/>` nested in markup is caught, not only a top-level router tag. Unbound
677
- // names are injected at Program.exit.
678
- if (!resolvedByRule && t.isIdentifier(comp) && ROUTER_COMPONENTS.has(comp.name)) {
679
- state.controlFlowComponents.add(comp.name);
680
- state.needsControlFlowImport = true;
681
- }
682
- if (state.backend === 'imperative') {
683
- state.needsCreateComponent = true;
684
- state.needsCreateMemo = true;
685
- return t.callExpression(t.identifier('createMemo'), [
686
- t.arrowFunctionExpression([], t.callExpression(t.identifier('createComponent'), [comp, propsObj])),
687
- ]);
688
- }
689
- return t.callExpression(t.identifier('jsx'), [comp, propsObj]);
690
- }
691
- /**
692
- * IR-pipeline path (codegen: 'ir'): build host-agnostic IR from the JSX node, run
693
- * the imperative backend's emit(), and reparse the emitted code into an AST
694
- * expression. Records the runtime symbols used so Program.exit can import them.
695
- * Produces the same runtime calls as the inline path — this is the route a future
696
- * swc front-end reuses.
697
- */
698
332
  /**
699
333
  * Hoist the backend's static templates to module scope and rewrite the emitted code to
700
334
  * reference them. Deduped by HTML, so a subtree repeated in a file is parsed once.
@@ -722,7 +356,7 @@ function emitViaIR(node, state) {
722
356
  // `used` collects runtime symbols emitted for JSX nested inside expressions
723
357
  // (render-props, ternaries, fallback={<p/>}, …) so we import them too.
724
358
  const used = new Set();
725
- const ir = buildIR(node, used);
359
+ const ir = buildIR(node, used, nestedHook(state));
726
360
  // The same passes the lit path runs. Without them `codegen: 'ir'` silently skipped
727
361
  // resolution entirely — a resolved tag compiled to a reference with no import.
728
362
  resolveComponentSources(ir, {
@@ -785,7 +419,7 @@ function dedupeImports(imports) {
785
419
  function emitLitViaIR(node, state) {
786
420
  const used = new Set();
787
421
  const svg = t.isIdentifier(node.tag) && node.tag.name === 'svg';
788
- const ir = buildIRFromLit(node, used, { svg });
422
+ const ir = buildIRFromLit(node, used, { svg, onNested: nestedHook(state) });
789
423
  // Resolve pass: annotate each component reference with where it comes from, before
790
424
  // codegen. Bindings are not checked here — Program.exit knows the whole file's scope
791
425
  // and drops any injected import whose name turns out to be bound.
@@ -897,870 +531,3 @@ function collectRuntimeNeeds(node, state) {
897
531
  collectRuntimeNeeds(c, state);
898
532
  }
899
533
  }
900
- //This method will convert props to lazy props like
901
- function lazyProps(properties) {
902
- return properties.map((prop) => {
903
- // Only transform normal properties (e.g., { open: variable })
904
- // Skip methods, spread elements, or already existing getters
905
- if (t.isObjectProperty(prop)) {
906
- // Handle potential computed properties (like {[someKey]: variable})
907
- const key = prop.key;
908
- const computed = prop.computed;
909
- const value = prop.value;
910
- // Create a getter method: get key() { return value; }
911
- return t.objectMethod('get', // Kind of method
912
- key, // Property key
913
- [], // Getters take no arguments
914
- t.blockStatement([
915
- // Body: { return value; }
916
- t.returnStatement(value),
917
- ]), computed // Preserve computed status if true
918
- );
919
- }
920
- // Keep existing ObjectMethods (like get already there, or regular methods) as-is
921
- return prop;
922
- });
923
- }
924
- ;
925
- function transformAutomaticElement(element, state, development, autoShow = true) {
926
- const openingElement = element.openingElement;
927
- const children = element.children;
928
- // Get element name/tag
929
- const tag = getElementName(openingElement.name);
930
- // Only auto-wrap reactive expressions for native HTML elements (lowercase tag).
931
- // Component props must NOT be wrapped — the component receives the value
932
- // as-is and is responsible for its own reactivity.
933
- const isNativeElement = t.isJSXIdentifier(openingElement.name) &&
934
- /^[a-z]/.test(openingElement.name.name);
935
- // Transform attributes to props object
936
- const attrs = transformAttributes(openingElement.attributes, state, isNativeElement, t.isJSXIdentifier(openingElement.name) ? openingElement.name.name : '');
937
- let props = attrs.props;
938
- const spreads = attrs.spreads;
939
- // Component props become lazy getters so the component is created ONCE and reactivity flows
940
- // through the getter (a reactive `prop={sig()}` read eagerly would re-create the whole
941
- // component via the enclosing createMemo, so a <Portal> re-mounts and piles up). Static props
942
- // as getters are harmless. Native-element attrs must NOT be wrapped: they already carry their
943
- // own reactivity as `() => expr` thunks that `spread` consumes.
944
- if (!isNativeElement)
945
- props = lazyProps(props);
946
- // Transform children
947
- const transformedChildren = transformChildren(children, state, autoShow);
948
- // Imperative backend: emit direct runtime calls instead of jsx()/jsxs().
949
- if (state.backend === 'imperative') {
950
- return emitImperativeElement(tag, isNativeElement, props, spreads, transformedChildren, state);
951
- }
952
- // Add children to props if any
953
- if (transformedChildren.length > 0) {
954
- const childrenProp = transformedChildren.length === 1
955
- ? transformedChildren[0]
956
- : t.arrayExpression(transformedChildren);
957
- props.push(t.objectProperty(t.identifier('children'), childrenProp));
958
- }
959
- // Build props object with spreads
960
- let propsObj;
961
- if (spreads.length > 0) {
962
- // Use mergeProps helper to preserve reactivity for store proxies
963
- const baseProps = props.length > 0 ? t.objectExpression(props) : t.objectExpression([]);
964
- // Import mergeProps from jsx-runtime if spreads are used
965
- state.needsMergeProps = true;
966
- propsObj = t.callExpression(t.identifier('mergeProps'), [
967
- ...spreads,
968
- baseProps,
969
- ]);
970
- }
971
- else {
972
- propsObj = t.objectExpression(props);
973
- }
974
- // Determine function name
975
- const functionName = development
976
- ? 'jsxDEV'
977
- : transformedChildren.length > 1
978
- ? 'jsxs'
979
- : 'jsx';
980
- // Build jsx call
981
- const args = [tag, propsObj];
982
- if (development) {
983
- // Add key, isStaticChildren, source, self for dev mode
984
- args.push(t.identifier('undefined'), // key
985
- t.booleanLiteral(false), // isStaticChildren
986
- t.identifier('undefined'), // source
987
- t.identifier('undefined') // self
988
- );
989
- }
990
- return t.callExpression(t.identifier(functionName), args);
991
- }
992
- function transformClassicElement(element, state, pragma) {
993
- const openingElement = element.openingElement;
994
- const children = element.children;
995
- const tag = getElementName(openingElement.name);
996
- const isNativeElement = t.isJSXIdentifier(openingElement.name) &&
997
- /^[a-z]/.test(openingElement.name.name);
998
- const { props, spreads } = transformAttributes(openingElement.attributes, state, isNativeElement, t.isJSXIdentifier(openingElement.name) ? openingElement.name.name : '');
999
- const transformedChildren = transformChildren(children, state, state.autoShow ?? true);
1000
- let propsObj;
1001
- if (spreads.length > 0) {
1002
- // Use mergeProps helper to preserve reactivity for store proxies
1003
- // mergeProps(...spreads, baseProps) instead of Object.assign
1004
- const baseProps = props.length > 0 ? t.objectExpression(props) : t.objectExpression([]);
1005
- // Import mergeProps from jsx-runtime if spreads are used
1006
- state.needsMergeProps = true;
1007
- propsObj = t.callExpression(t.identifier('mergeProps'), [
1008
- ...spreads,
1009
- baseProps,
1010
- ]);
1011
- }
1012
- else {
1013
- propsObj = props.length > 0 ? t.objectExpression(props) : t.nullLiteral();
1014
- }
1015
- const args = [tag, propsObj, ...transformedChildren];
1016
- return t.callExpression(t.identifier(pragma), args);
1017
- }
1018
- function transformJSXFragment(fragment, state, options) {
1019
- const { runtime, pragmaFrag, development } = options;
1020
- const children = transformChildren(fragment.children, state, state.autoShow ?? true);
1021
- // Imperative backend: a fragment is just its children; insert() flattens arrays.
1022
- if (state.backend === 'imperative') {
1023
- if (children.length === 0)
1024
- return t.nullLiteral();
1025
- return childrenValue(children);
1026
- }
1027
- if (runtime === 'automatic') {
1028
- const functionName = development
1029
- ? 'jsxDEV'
1030
- : children.length > 1
1031
- ? 'jsxs'
1032
- : 'jsx';
1033
- const props = t.objectExpression([
1034
- t.objectProperty(t.identifier('children'), children.length === 1 ? children[0] : t.arrayExpression(children)),
1035
- ]);
1036
- const args = [t.identifier('Fragment'), props];
1037
- if (development) {
1038
- args.push(t.identifier('undefined'), t.booleanLiteral(false), t.identifier('undefined'), t.identifier('undefined'));
1039
- }
1040
- return t.callExpression(t.identifier(functionName), args);
1041
- }
1042
- else {
1043
- return t.callExpression(t.identifier(pragmaFrag), children);
1044
- }
1045
- }
1046
- // ============================================================================
1047
- // Element Name Transformation
1048
- // ============================================================================
1049
- function getElementName(name) {
1050
- if (t.isJSXIdentifier(name)) {
1051
- const nameStr = name.name;
1052
- // Check if it's a native element (lowercase)
1053
- if (nameStr[0] === nameStr[0].toLowerCase()) {
1054
- return t.stringLiteral(nameStr);
1055
- }
1056
- // Component reference
1057
- return t.identifier(nameStr);
1058
- }
1059
- if (t.isJSXMemberExpression(name)) {
1060
- return transformJSXMemberExpression(name);
1061
- }
1062
- if (t.isJSXNamespacedName(name)) {
1063
- return t.stringLiteral(`${name.namespace.name}:${name.name.name}`);
1064
- }
1065
- return t.stringLiteral('div');
1066
- }
1067
- function transformJSXMemberExpression(expr) {
1068
- let object;
1069
- if (t.isJSXIdentifier(expr.object)) {
1070
- object = t.identifier(expr.object.name);
1071
- }
1072
- else {
1073
- object = transformJSXMemberExpression(expr.object);
1074
- }
1075
- return t.memberExpression(object, t.identifier(expr.property.name));
1076
- }
1077
- // ============================================================================
1078
- // Attribute Transformation
1079
- // ============================================================================
1080
- /** Returns true when `name` can be used as an unquoted JS identifier. */
1081
- function isValidJSIdentifier(name) {
1082
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
1083
- }
1084
- function transformAttributes(attributes, state, isNativeElement = true, tagName = '') {
1085
- const props = [];
1086
- const spreads = [];
1087
- const cfValueProp = CONTROL_FLOW_VALUE_PROPS[tagName];
1088
- // `use:name` directives, collected into the single `use` prop the runtime dispatches on.
1089
- const usePairs = [];
1090
- // `load:` is a directive, not a prop — it says when this component's module is
1091
- // fetched. Cleared per element so a strategy cannot leak onto the next tag.
1092
- state.pendingLoad = undefined;
1093
- for (const attr of attributes) {
1094
- if (t.isJSXSpreadAttribute(attr)) {
1095
- // Collect spread expressions separately
1096
- spreads.push(attr.argument);
1097
- }
1098
- else if (t.isJSXAttribute(attr)) {
1099
- const name = getAttributeName(attr.name);
1100
- if (isLoadDirective(name)) {
1101
- // Same parser the html`` lowering uses, so the two syntaxes cannot drift.
1102
- state.pendingLoad = readLoadDirective(name, attr);
1103
- continue;
1104
- }
1105
- // use:name / use:name={opts} -> one `use` prop holding [directive, () => opts] pairs.
1106
- // Without this the attribute reached the runtime as a prop literally named "use:name",
1107
- // which nothing dispatches on — so the directive silently never ran. The IR path has
1108
- // always normalised it; this is the same normalisation for the inline path.
1109
- if (t.isJSXNamespacedName(attr.name) && attr.name.namespace.name === 'use') {
1110
- const directive = t.identifier(attr.name.name.name);
1111
- const opts = attr.value === null ? null : transformAttributeValue(attr.value, state, name, isNativeElement);
1112
- usePairs.push(t.arrayExpression(
1113
- // The options are wrapped in an arrow so the directive can track them, matching
1114
- // what the runtime's applyUse expects of the second slot.
1115
- opts ? [directive, t.arrowFunctionExpression([], opts)] : [directive]));
1116
- continue;
1117
- }
1118
- const value = transformAttributeValue(attr.value, state, name, isNativeElement);
1119
- // Detect event handlers for delegation
1120
- if (EVENT_PROPS.test(name)) {
1121
- const eventName = name.slice(2).toLowerCase();
1122
- if (DELEGATABLE_EVENTS.includes(eventName)) {
1123
- state.delegatedEvents.add(eventName);
1124
- }
1125
- }
1126
- const key = isValidJSIdentifier(name)
1127
- ? t.identifier(name)
1128
- : t.stringLiteral(name);
1129
- // Inline JSX passed to a COMPONENT prop (e.g. <Suspense fallback={<div/>}>) becomes a
1130
- // LAZY getter — same as children. Otherwise it's an eager IIFE built when the props object
1131
- // is constructed; during hydration that walks the hydration cursor and adopts the real
1132
- // server node (a fallback `<div>` matches the content `<div>` by tag), duplicating the
1133
- // tree. As a getter the node is only built when the component actually reads the prop.
1134
- const raw = attr.value;
1135
- const isInlineJsx = t.isJSXElement(raw) ||
1136
- t.isJSXFragment(raw) ||
1137
- (t.isJSXExpressionContainer(raw) &&
1138
- (t.isJSXElement(raw.expression) || t.isJSXFragment(raw.expression)));
1139
- // Control-flow reactive value prop (each/when/component/mount): emit as a getter so
1140
- // reading it re-runs the caller's expression inside the component's tracking scope.
1141
- const isCfValueProp = !isNativeElement && name === cfValueProp;
1142
- // A reactive component prop (`prop={sig()}`, `prop={store.x}`) must be a getter, not an
1143
- // eager value: eager reads the signal inside the enclosing `createMemo(() =>
1144
- // createComponent(Comp, props))`, so the memo re-creates the WHOLE component on every
1145
- // change (a <Portal> then re-mounts and piles up). As a getter the component is created
1146
- // once and reactivity flows through the getter into the component's own effects.
1147
- const rawExpr = t.isJSXExpressionContainer(raw) && !t.isJSXEmptyExpression(raw.expression)
1148
- ? raw.expression
1149
- : null;
1150
- const isReactiveComponentProp = !isNativeElement && !!rawExpr && shouldAutoWrapAttrExpression(rawExpr, name);
1151
- if ((!isNativeElement && isInlineJsx) || isCfValueProp || isReactiveComponentProp) {
1152
- props.push(t.objectMethod('get', key, [], t.blockStatement([t.returnStatement(value)])));
1153
- }
1154
- else {
1155
- props.push(t.objectProperty(key, value));
1156
- }
1157
- }
1158
- }
1159
- if (usePairs.length > 0) {
1160
- props.push(t.objectProperty(t.identifier('use'), t.arrayExpression(usePairs)));
1161
- }
1162
- return { props, spreads };
1163
- }
1164
- function getAttributeName(name) {
1165
- if (t.isJSXIdentifier(name)) {
1166
- return name.name === 'class' ? 'className' : name.name;
1167
- }
1168
- if (t.isJSXNamespacedName(name)) {
1169
- return `${name.namespace.name}:${name.name.name}`;
1170
- }
1171
- return 'unknown';
1172
- }
1173
- function transformAttributeValue(value, state, attrName = '', isNativeElement = true) {
1174
- if (value === null) {
1175
- return t.booleanLiteral(true);
1176
- }
1177
- if (t.isStringLiteral(value)) {
1178
- // JSX allows string attribute values to span multiple lines, but JS string
1179
- // literals cannot contain raw newlines. When multi-line, normalise: collapse
1180
- // any run of whitespace (including newlines) to a single space and trim.
1181
- // We create a *new* StringLiteral so Babel discards the original extra.raw
1182
- // (which still contains the literal newlines) and generates a clean literal.
1183
- if (value.value.includes('\n')) {
1184
- return t.stringLiteral(value.value.replace(/\s+/g, ' ').trim());
1185
- }
1186
- return value;
1187
- }
1188
- if (t.isJSXExpressionContainer(value)) {
1189
- if (t.isJSXEmptyExpression(value.expression)) {
1190
- return t.booleanLiteral(true);
1191
- }
1192
- const expr = value.expression;
1193
- // Auto-wrap reactive expressions so DOM attribute reads happen inside effects.
1194
- // Only applies to native HTML elements — component props must NOT be wrapped
1195
- // because the component receives the value directly and calling it would give
1196
- // () => value instead of value.
1197
- if (isNativeElement && shouldAutoWrapAttrExpression(expr, attrName)) {
1198
- return t.arrowFunctionExpression([], expr);
1199
- }
1200
- return expr;
1201
- }
1202
- if (t.isJSXElement(value)) {
1203
- return transformAutomaticElement(value, state, false, state.autoShow ?? true);
1204
- }
1205
- if (t.isJSXFragment(value)) {
1206
- return transformJSXFragment(value, state, {
1207
- runtime: 'automatic',
1208
- pragmaFrag: 'Fragment',
1209
- development: false,
1210
- });
1211
- }
1212
- return t.booleanLiteral(true);
1213
- }
1214
- /**
1215
- * Decide whether a JSX attribute value expression should be auto-wrapped
1216
- * in `() => expr` for fine-grained reactivity (SolidJS style).
1217
- */
1218
- function shouldAutoWrapAttrExpression(expr, attrName) {
1219
- // Event handlers must NOT be wrapped — they are called by the runtime.
1220
- // Covers both camelCase (onClick) and namespaced (on:click) forms.
1221
- if (EVENT_PROPS.test(attrName) || attrName.startsWith('on:')) {
1222
- return false;
1223
- }
1224
- // ref receives the DOM element directly — never wrap.
1225
- if (attrName === 'ref') {
1226
- return false;
1227
- }
1228
- // use:Directive — the directive itself decides how to handle the value.
1229
- if (attrName.startsWith('use:')) {
1230
- return false;
1231
- }
1232
- // Already an arrow function or function expression — already reactive.
1233
- if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
1234
- return false;
1235
- }
1236
- // Plain identifier — may itself be a signal accessor/function; let runtime handle it.
1237
- if (t.isIdentifier(expr)) {
1238
- return false;
1239
- }
1240
- // Static literals need no wrapping.
1241
- if (t.isStringLiteral(expr) ||
1242
- t.isNumericLiteral(expr) ||
1243
- t.isBooleanLiteral(expr) ||
1244
- t.isNullLiteral(expr)) {
1245
- return false;
1246
- }
1247
- // Call expressions: signal(), count(), store.method()
1248
- if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
1249
- return true;
1250
- }
1251
- // Member expressions: store.prop, obj.value
1252
- if (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
1253
- return true;
1254
- }
1255
- // Compound expressions that may contain reactive reads.
1256
- if (t.isLogicalExpression(expr) ||
1257
- t.isConditionalExpression(expr) ||
1258
- t.isBinaryExpression(expr) ||
1259
- t.isUnaryExpression(expr)) {
1260
- return true;
1261
- }
1262
- // Template literals: `Hello ${name()}`
1263
- if (t.isTemplateLiteral(expr)) {
1264
- return true;
1265
- }
1266
- // Object expressions: classList={{ active: isActive() }}
1267
- if (t.isObjectExpression(expr)) {
1268
- return true;
1269
- }
1270
- // Array expressions: style={[baseStyle, dynamicStyle()]}
1271
- if (t.isArrayExpression(expr)) {
1272
- return true;
1273
- }
1274
- return false;
1275
- }
1276
- // ============================================================================
1277
- // Children Transformation
1278
- // ============================================================================
1279
- /**
1280
- * Canonical JSX text cleaning (matches babel/React/Solid): trim line-boundary
1281
- * whitespace, collapse runs to single spaces, and drop text that is only
1282
- * whitespace spanning a newline. Returns '' when the node is insignificant.
1283
- */
1284
- function cleanJSXText(value) {
1285
- const lines = value.split(/\r\n|\n|\r/);
1286
- let lastNonEmpty = 0;
1287
- for (let i = 0; i < lines.length; i++) {
1288
- if (/[^ \t]/.test(lines[i]))
1289
- lastNonEmpty = i;
1290
- }
1291
- let out = '';
1292
- for (let i = 0; i < lines.length; i++) {
1293
- let line = lines[i].replace(/\t/g, ' ');
1294
- if (i !== 0)
1295
- line = line.replace(/^ +/, '');
1296
- if (i !== lines.length - 1)
1297
- line = line.replace(/ +$/, '');
1298
- if (!line)
1299
- continue;
1300
- if (i !== lastNonEmpty)
1301
- line += ' ';
1302
- out += line;
1303
- }
1304
- return out;
1305
- }
1306
- function transformChildren(children, state, autoShow = true) {
1307
- const result = [];
1308
- for (const child of children) {
1309
- if (t.isJSXText(child)) {
1310
- // Canonical JSX whitespace handling (as React/Solid): whitespace-only text
1311
- // that spans a line break is insignificant and dropped; inline runs collapse
1312
- // to a single space. This matters beyond cosmetics — keeping a `" "` text
1313
- // node next to a child turns `props.children` into an ARRAY, and an array
1314
- // insert doesn't reactively bind a memo/component sitting inside it (so a
1315
- // nested lazy/<Suspense> never updates). Dropping the stray whitespace keeps
1316
- // a lone child a lone child, so insert binds it reactively.
1317
- const text = cleanJSXText(child.value);
1318
- if (text)
1319
- result.push(t.stringLiteral(text));
1320
- }
1321
- else if (t.isJSXExpressionContainer(child)) {
1322
- if (!t.isJSXEmptyExpression(child.expression)) {
1323
- const expr = child.expression;
1324
- // Auto-transform patterns into jsx(Show, ...) so JSX subtrees are
1325
- // created once and never recreated on every signal change.
1326
- //
1327
- // Handled patterns:
1328
- // () => cond && <JSX /> — arrow logical
1329
- // () => cond ? <JSX /> : fallback — arrow ternary
1330
- // cond && <JSX /> — bare logical
1331
- // cond ? <JSX /> : fallback — bare ternary
1332
- if (autoShow) {
1333
- const showNode = tryTransformArrowToShow(expr, state) ??
1334
- tryTransformRawToShow(expr, state);
1335
- if (showNode) {
1336
- result.push(showNode);
1337
- continue;
1338
- }
1339
- }
1340
- else {
1341
- // autoShow off: wrap JSX conditional expressions in createMemo(() => expr)
1342
- // so the condition is still evaluated reactively without recreating the tree.
1343
- const memoNode = tryWrapConditionalAsMemo(expr, state);
1344
- if (memoNode) {
1345
- result.push(memoNode);
1346
- continue;
1347
- }
1348
- }
1349
- // Auto-wrap reactive expressions for SolidJS-style reactivity
1350
- if (shouldAutoWrapExpression(expr)) {
1351
- result.push(t.arrowFunctionExpression([], expr));
1352
- }
1353
- else {
1354
- result.push(expr);
1355
- }
1356
- }
1357
- }
1358
- else if (t.isJSXElement(child)) {
1359
- result.push(transformAutomaticElement(child, state, false, state.autoShow ?? true));
1360
- }
1361
- else if (t.isJSXFragment(child)) {
1362
- result.push(transformJSXFragment(child, state, {
1363
- runtime: 'automatic',
1364
- pragmaFrag: 'Fragment',
1365
- development: false,
1366
- }));
1367
- }
1368
- else if (t.isJSXSpreadChild(child)) {
1369
- result.push(child.expression);
1370
- }
1371
- }
1372
- return result;
1373
- }
1374
- // ============================================================================
1375
- // Auto-Show Transform
1376
- // ============================================================================
1377
- /**
1378
- * Detects the patterns:
1379
- * () => condition && <JSX />
1380
- * () => condition ? <JSX /> : null
1381
- * () => condition ? <JSX /> : <Fallback />
1382
- *
1383
- * and rewrites them to:
1384
- * jsx(Show, { when: () => condition, children: jsx(...), fallback? })
1385
- *
1386
- * This prevents the JSX subtree from being recreated on every reactive update.
1387
- * Show's memo only re-evaluates when the boolean result of `condition` changes.
1388
- */
1389
- function tryTransformArrowToShow(expr, state) {
1390
- if (!t.isArrowFunctionExpression(expr) ||
1391
- expr.params.length !== 0 ||
1392
- expr.async) {
1393
- return null;
1394
- }
1395
- const body = expr.body;
1396
- // Pattern 1: () => condition && <JSX /> (or () => condition && (() => <JSX />))
1397
- if (t.isLogicalExpression(body) &&
1398
- body.operator === '&&') {
1399
- const jsx = getJSXContent(body.right);
1400
- if (jsx) {
1401
- return buildShowCall(unwrapZeroArgArrow(body.left), jsx, null, state);
1402
- }
1403
- }
1404
- // Pattern 2: () => condition ? <JSX /> : null | undefined | false
1405
- // Pattern 3: () => condition ? <JSX /> : <Fallback />
1406
- // Pattern 4: () => condition ? <JSX /> : otherCond ? <JSX2 /> : <Fallback /> (nested ternary)
1407
- if (t.isConditionalExpression(body)) {
1408
- const jsx = getJSXContent(body.consequent);
1409
- if (jsx) {
1410
- const fallback = buildFallback(body.alternate, state);
1411
- return buildShowCall(unwrapZeroArgArrow(body.test), jsx, fallback, state, true);
1412
- }
1413
- }
1414
- return null;
1415
- }
1416
- /**
1417
- * Handles bare (non-arrow-wrapped) ternary and logical JSX expressions:
1418
- *
1419
- * cond && <JSX />
1420
- * cond ? <JSX /> : fallback
1421
- *
1422
- * These are transformed to `jsx(Show, { when: () => cond, ... })` exactly like
1423
- * their arrow-wrapped counterparts, preventing JSX subtrees from being
1424
- * recreated on every reactive update.
1425
- */
1426
- function tryTransformRawToShow(expr, state) {
1427
- // cond && <JSX /> (or cond && (() => <JSX />))
1428
- if (t.isLogicalExpression(expr) && expr.operator === '&&') {
1429
- const jsx = getJSXContent(expr.right);
1430
- if (jsx) {
1431
- return buildShowCall(unwrapZeroArgArrow(expr.left), jsx, null, state);
1432
- }
1433
- }
1434
- // cond ? <JSX /> : fallback (consequent may be () => <JSX />)
1435
- // test may be (() => expr) — unwrap to avoid double arrow wrapping
1436
- if (t.isConditionalExpression(expr)) {
1437
- const jsx = getJSXContent(expr.consequent);
1438
- if (jsx) {
1439
- const fallback = buildFallback(expr.alternate, state);
1440
- return buildShowCall(unwrapZeroArgArrow(expr.test), jsx, fallback, state);
1441
- }
1442
- }
1443
- return null;
1444
- }
1445
- /**
1446
- * When autoShow is disabled, wraps ALL ternary and logical-AND expressions in
1447
- * `createMemo(() => normalizedExpr)` so the condition evaluates reactively and
1448
- * the result is cached (not re-evaluated on every render cycle).
1449
- *
1450
- * Normalization rules applied before wrapping:
1451
- * - Outer zero-arg arrow unwrapped: `() => cond ? a : b` → body used directly
1452
- * - Arrow-wrapped test unwrapped: `(() => cond) ? a : b` → `cond ? a : b`
1453
- * - Arrow-wrapped JSX consequent: `() => <JSX>` → `<JSX>` (so Babel transforms it)
1454
- * - Arrow-wrapped JSX alternate: `() => <JSX>` → `<JSX>` likewise
1455
- * - Non-JSX branches left as-is: strings, identifiers, call results, etc.
1456
- *
1457
- * Examples:
1458
- * {cond ? "a" : "b"} → createMemo(() => cond ? "a" : "b")
1459
- * {cond ? <A/> : <B/>} → createMemo(() => cond ? <A/> : <B/>)
1460
- * {(() => cond) ? ()=><A/> : <B/>} → createMemo(() => cond ? <A/> : <B/>)
1461
- * {cond && <A/>} → createMemo(() => cond && <A/>)
1462
- */
1463
- function tryWrapConditionalAsMemo(expr, state) {
1464
- // Unwrap outer zero-arg arrow: () => <inner> → work on inner directly
1465
- const inner = t.isArrowFunctionExpression(expr) &&
1466
- expr.params.length === 0 &&
1467
- !expr.async &&
1468
- t.isExpression(expr.body)
1469
- ? expr.body
1470
- : expr;
1471
- if (t.isConditionalExpression(inner)) {
1472
- // Unwrap arrow-wrapped test to prevent always-truthy condition
1473
- const normalizedTest = unwrapZeroArgArrow(inner.test);
1474
- // Unwrap arrow-wrapped JSX branches so Babel can transform them;
1475
- // non-JSX branches (strings, calls, etc.) are kept as-is
1476
- const consequentJsx = getJSXContent(inner.consequent);
1477
- const normalizedConsequent = consequentJsx ?? inner.consequent;
1478
- const altJsx = getJSXContent(inner.alternate);
1479
- const normalizedAlternate = altJsx ?? inner.alternate;
1480
- const normalized = t.conditionalExpression(normalizedTest, normalizedConsequent, normalizedAlternate);
1481
- // If either branch is a control-flow component (For, Switch, Index, Show, …),
1482
- // do NOT wrap in createMemo — return null so the caller falls through to the
1483
- // plain `() => expr` path via shouldAutoWrapExpression.
1484
- if (isControlFlowJSX(inner.consequent) ||
1485
- isControlFlowJSX(inner.alternate)) {
1486
- return null;
1487
- }
1488
- state.needsCreateMemo = true;
1489
- return t.callExpression(t.identifier('createMemo'), [
1490
- t.arrowFunctionExpression([], normalized),
1491
- ]);
1492
- }
1493
- if (t.isLogicalExpression(inner) && inner.operator === '&&') {
1494
- const normalizedLeft = unwrapZeroArgArrow(inner.left);
1495
- const rightJsx = getJSXContent(inner.right);
1496
- const normalizedRight = rightJsx ?? inner.right;
1497
- const normalized = t.logicalExpression('&&', normalizedLeft, normalizedRight);
1498
- // Same guard for logical && (e.g. {show && <For ...>})
1499
- if (isControlFlowJSX(inner.right)) {
1500
- return null;
1501
- }
1502
- state.needsCreateMemo = true;
1503
- return t.callExpression(t.identifier('createMemo'), [
1504
- t.arrowFunctionExpression([], normalized),
1505
- ]);
1506
- }
1507
- return null;
1508
- }
1509
- /**
1510
- * Recursively transforms a fallback expression into a reactive value.
1511
- * If the expression is a nested ternary `cond ? <JSX /> : rest`, it is
1512
- * transformed into a nested `Show` call so the condition is tracked reactively.
1513
- */
1514
- function buildFallback(expr, state) {
1515
- if (isFalsyNode(expr))
1516
- return null;
1517
- // bare JSX or () => <JSX>
1518
- const directJsx = getJSXContent(expr);
1519
- if (directJsx) {
1520
- return transformJSXNode(directJsx, state);
1521
- }
1522
- // Nested ternary whose truthy branch is JSX (or arrow-wrapped JSX)
1523
- if (t.isConditionalExpression(expr)) {
1524
- const inner = expr;
1525
- const innerJsx = getJSXContent(inner.consequent);
1526
- if (innerJsx) {
1527
- const nestedFallback = buildFallback(inner.alternate, state);
1528
- return buildShowCall(unwrapZeroArgArrow(inner.test), innerJsx, nestedFallback, state);
1529
- }
1530
- }
1531
- return expr;
1532
- }
1533
- function isJSXNode(node) {
1534
- return t.isJSXElement(node) || t.isJSXFragment(node);
1535
- }
1536
- /**
1537
- * Returns true when `expr` (or a zero-arg arrow wrapping it) is a JSXElement
1538
- * whose tag is one of the Fluixi control-flow components (For, Switch, Index,
1539
- * Show, Portal, Dynamic, ErrorBoundary, Suspense). These must NOT be wrapped in
1540
- * createMemo — a plain `() => expr` arrow is sufficient and avoids double-tracking.
1541
- */
1542
- function isControlFlowJSX(expr) {
1543
- const jsx = getJSXContent(expr);
1544
- if (!jsx || !t.isJSXElement(jsx))
1545
- return false;
1546
- const name = jsx.openingElement.name;
1547
- return t.isJSXIdentifier(name) && CONTROL_FLOW_COMPONENTS.has(name.name);
1548
- }
1549
- /**
1550
- * Returns the JSX content from either bare `<JSX>` or a zero-arg arrow `() => <JSX>`.
1551
- * This allows Show transformation to work regardless of whether the author wrapped the
1552
- * consequent/alternate in an arrow function or not.
1553
- */
1554
- function getJSXContent(expr) {
1555
- if (isJSXNode(expr))
1556
- return expr;
1557
- if (t.isArrowFunctionExpression(expr) &&
1558
- expr.params.length === 0 &&
1559
- !expr.async &&
1560
- isJSXNode(expr.body)) {
1561
- return expr.body;
1562
- }
1563
- return null;
1564
- }
1565
- /**
1566
- * Unwraps a zero-argument arrow `() => expr` to `expr`.
1567
- * Used to avoid double-wrapping when the test/condition is already `() => cond`.
1568
- * `buildShowCall` always wraps the condition in `() =>`, so passing a bare expr
1569
- * produces `() => cond`; passing `() => cond` without unwrapping produces `() => () => cond`
1570
- * which is always truthy.
1571
- */
1572
- function unwrapZeroArgArrow(expr) {
1573
- if (t.isArrowFunctionExpression(expr) &&
1574
- expr.params.length === 0 &&
1575
- !expr.async &&
1576
- t.isExpression(expr.body)) {
1577
- return expr.body;
1578
- }
1579
- return expr;
1580
- }
1581
- function isFalsyNode(node) {
1582
- return (t.isNullLiteral(node) ||
1583
- (t.isIdentifier(node) && node.name === 'undefined') ||
1584
- (t.isBooleanLiteral(node) && node.value === false));
1585
- }
1586
- function transformJSXNode(node, state) {
1587
- if (t.isJSXElement(node)) {
1588
- return transformAutomaticElement(node, state, false, state.autoShow ?? true);
1589
- }
1590
- return transformJSXFragment(node, state, {
1591
- runtime: 'automatic',
1592
- pragmaFrag: 'Fragment',
1593
- development: false,
1594
- });
1595
- }
1596
- function buildShowCall(condition, children, fallback, state, fromArrow = false) {
1597
- // Register Show so it gets auto-imported if needed
1598
- state.controlFlowComponents.add('Show');
1599
- state.needsControlFlowImport = true;
1600
- const transformedChildren = transformJSXNode(children, state);
1601
- // console.log('build show call', generate(condition).code);
1602
- const props = [
1603
- t.objectProperty(t.identifier('when'), t.arrowFunctionExpression([], condition)
1604
- // fromArrow ? (condition) : t.arrowFunctionExpression([], condition),
1605
- ),
1606
- t.objectProperty(t.identifier('children'), transformedChildren),
1607
- ];
1608
- if (fallback) {
1609
- props.push(t.objectProperty(t.identifier('fallback'), fallback));
1610
- }
1611
- return emitComponentCall(t.identifier('Show'), t.objectExpression(props), state);
1612
- }
1613
- // ============================================================================
1614
- // Reactivity Detection
1615
- // ============================================================================
1616
- /**
1617
- * Check if an expression should be auto-wrapped in an arrow function for reactivity
1618
- * This enables SolidJS-style reactivity where you can write {store.property} or {signal()}
1619
- * without manual wrapping
1620
- */
1621
- function shouldAutoWrapExpression(expr) {
1622
- // Member expressions: store.property, user.name, etc.
1623
- // These need wrapping so property access happens inside an effect
1624
- if (t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) {
1625
- // `props.children` (any `.children`) is a stable subtree reference, not a
1626
- // reactive value. Wrapping it in `() => props.children` adds an insert layer
1627
- // that fully unwraps the children's Suspense/memo to a static node, severing
1628
- // the reactive binding — so a nested lazy/<Suspense> that resolves later
1629
- // never propagates up through the component-children hop. Pass it through
1630
- // non-reactive (insert(el, props.children)) so insert binds to the child's
1631
- // own memo, exactly like a directly-rendered <Outlet/>.
1632
- if (!expr.computed && t.isIdentifier(expr.property, { name: 'children' })) {
1633
- return false;
1634
- }
1635
- return true;
1636
- }
1637
- // Call expressions: signal(), count(), etc.
1638
- // These are already function calls but need wrapping to re-run on updates
1639
- if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
1640
- return true;
1641
- }
1642
- // DON'T wrap plain identifiers - they might be signal accessors or functions
1643
- // that should be passed as-is. Only wrap when accessing properties or calling.
1644
- if (t.isIdentifier(expr)) {
1645
- return false;
1646
- }
1647
- // Logical expressions: signal() && other, store.value || default
1648
- if (t.isLogicalExpression(expr) || t.isConditionalExpression(expr)) {
1649
- return true;
1650
- }
1651
- // Binary expressions: signal() + 1, store.count * 2
1652
- if (t.isBinaryExpression(expr)) {
1653
- return true;
1654
- }
1655
- // Unary expressions: !signal(), -store.count
1656
- if (t.isUnaryExpression(expr)) {
1657
- return true;
1658
- }
1659
- // Template literals: `Count: ${count()}`
1660
- if (t.isTemplateLiteral(expr)) {
1661
- return true;
1662
- }
1663
- // Don't wrap literals, strings, numbers, etc.
1664
- return false;
1665
- }
1666
- // ============================================================================
1667
- // Static Optimization
1668
- // ============================================================================
1669
- function shouldWrapReactive(expr, state) {
1670
- // Call expressions ending with () - likely signal accessors
1671
- if (t.isCallExpression(expr) && t.isIdentifier(expr.callee)) {
1672
- return true;
1673
- }
1674
- // Arrow functions with no parameters - could be computed values
1675
- if (t.isArrowFunctionExpression(expr) && expr.params.length === 0) {
1676
- return true;
1677
- }
1678
- // Function expressions with no parameters
1679
- if (t.isFunctionExpression(expr) && expr.params.length === 0) {
1680
- return true;
1681
- }
1682
- // Member expressions - could be store access
1683
- if (t.isMemberExpression(expr)) {
1684
- return true;
1685
- }
1686
- return false;
1687
- }
1688
- function wrapReactiveExpression(expr) {
1689
- return t.callExpression(t.identifier(''), [
1690
- t.arrowFunctionExpression([], expr),
1691
- ]);
1692
- }
1693
- // ============================================================================
1694
- // Static Element Optimization
1695
- // ============================================================================
1696
- function isStaticElement(element, state) {
1697
- const openingElement = element.openingElement;
1698
- // Check if tag is static (string literal)
1699
- if (!t.isJSXIdentifier(openingElement.name)) {
1700
- return false;
1701
- }
1702
- const tagName = openingElement.name.name;
1703
- if (tagName[0] !== tagName[0].toLowerCase()) {
1704
- // Component, not a native element
1705
- return false;
1706
- }
1707
- // Check if all attributes are static
1708
- for (const attr of openingElement.attributes) {
1709
- if (t.isJSXSpreadAttribute(attr)) {
1710
- return false;
1711
- }
1712
- if (t.isJSXAttribute(attr)) {
1713
- const value = attr.value;
1714
- if (t.isJSXExpressionContainer(value)) {
1715
- if (!t.isStringLiteral(value.expression) &&
1716
- !t.isNumericLiteral(value.expression)) {
1717
- return false;
1718
- }
1719
- }
1720
- }
1721
- }
1722
- // Check if all children are static
1723
- for (const child of element.children) {
1724
- if (t.isJSXElement(child) && !isStaticElement(child, state)) {
1725
- return false;
1726
- }
1727
- if (t.isJSXExpressionContainer(child)) {
1728
- return false;
1729
- }
1730
- }
1731
- return true;
1732
- }
1733
- function hoistStaticElement(element, state) {
1734
- const varName = `_$static${state.staticCounter++}`;
1735
- const transformed = transformAutomaticElement(element, state, false);
1736
- state.staticElements.set(varName, transformed);
1737
- return t.identifier(varName);
1738
- }
1739
- // ============================================================================
1740
- // Control Flow Optimization
1741
- // ============================================================================
1742
- function optimizeControlFlowComponent(element, state) {
1743
- const componentName = element.openingElement.name.name;
1744
- // Optimize Show component
1745
- if (componentName === 'Show') {
1746
- return optimizeShowComponent(element, state);
1747
- }
1748
- // Optimize For component
1749
- if (componentName === 'For') {
1750
- return optimizeForComponent(element, state);
1751
- }
1752
- return null;
1753
- }
1754
- function optimizeShowComponent(element, state) {
1755
- // Extract when, fallback, and children
1756
- // Apply optimizations based on patterns
1757
- return null; // Placeholder for optimization logic
1758
- }
1759
- function optimizeForComponent(element, state) {
1760
- // Extract each and children
1761
- // Apply keyed reconciliation optimizations
1762
- return null; // Placeholder for optimization logic
1763
- }
1764
- // ============================================================================
1765
- // Exports
1766
- // ============================================================================