@mmapp/react-compiler 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (251) hide show
  1. package/ATOM-PIPELINE.md +144 -0
  2. package/README.md +88 -40
  3. package/dist/babel/index.js +113 -6
  4. package/dist/babel/index.mjs +2 -2
  5. package/dist/chunk-3USIFFE4.mjs +2190 -0
  6. package/dist/chunk-45YMGEVT.mjs +186 -0
  7. package/dist/chunk-4FN2AISW.mjs +148 -0
  8. package/dist/chunk-4OPI5L7G.mjs +2593 -0
  9. package/dist/chunk-4RYTKOOJ.mjs +186 -0
  10. package/dist/chunk-5RKTOVR5.mjs +244 -0
  11. package/dist/chunk-5YDMOO4X.mjs +214 -0
  12. package/dist/chunk-64ZWEMLJ.mjs +148 -0
  13. package/dist/chunk-6XP4KSWQ.mjs +2190 -0
  14. package/dist/chunk-72QWL54I.mjs +175 -0
  15. package/dist/chunk-7B4TRI7C.mjs +4835 -0
  16. package/dist/chunk-7ZKGHTNB.mjs +4952 -0
  17. package/dist/chunk-CIESM3BP.mjs +33 -0
  18. package/dist/chunk-DE3ZGQAC.mjs +148 -0
  19. package/dist/chunk-DMCY3BBG.mjs +1933 -0
  20. package/dist/chunk-DPIK3PJS.mjs +244 -0
  21. package/dist/chunk-E5IVH4RE.mjs +186 -0
  22. package/dist/chunk-E6FZNUR5.mjs +4953 -0
  23. package/dist/chunk-EJRBDQDP.mjs +2607 -0
  24. package/dist/chunk-ELO4TXJL.mjs +186 -0
  25. package/dist/chunk-FKRO52XH.mjs +3446 -0
  26. package/dist/chunk-FL4YAKU6.mjs +4941 -0
  27. package/dist/chunk-FYT47UBU.mjs +5076 -0
  28. package/dist/chunk-GCLGPOJZ.mjs +148 -0
  29. package/dist/chunk-GXB4JOP7.mjs +5072 -0
  30. package/dist/chunk-HFXOUMTD.mjs +175 -0
  31. package/dist/chunk-HWIZ47US.mjs +214 -0
  32. package/dist/chunk-IB7MNPQL.mjs +4953 -0
  33. package/dist/chunk-ICSIHQCG.mjs +148 -0
  34. package/dist/chunk-JLA5VNQ3.mjs +186 -0
  35. package/dist/chunk-JQLWFCTM.mjs +214 -0
  36. package/dist/chunk-KFJJCQAL.mjs +148 -0
  37. package/dist/chunk-KJUIIEQE.mjs +186 -0
  38. package/dist/chunk-KNWTHRVQ.mjs +175 -0
  39. package/dist/chunk-KSG4XSZF.mjs +175 -0
  40. package/dist/chunk-LF5N6DOU.mjs +175 -0
  41. package/dist/chunk-LJQCM2IM.mjs +214 -0
  42. package/dist/chunk-NW6555WJ.mjs +186 -0
  43. package/dist/chunk-OMZE6VLQ.mjs +214 -0
  44. package/dist/chunk-P4BR7WVO.mjs +2190 -0
  45. package/dist/chunk-QQHVYH2X.mjs +244 -0
  46. package/dist/chunk-S5QLWLLT.mjs +186 -0
  47. package/dist/chunk-SCWGT2FY.mjs +2190 -0
  48. package/dist/chunk-SMKJUSB3.mjs +2190 -0
  49. package/dist/chunk-VCAY2KGM.mjs +175 -0
  50. package/dist/chunk-WECAV6QB.mjs +148 -0
  51. package/dist/chunk-WMKBXUCE.mjs +3228 -0
  52. package/dist/chunk-XAJ5BKKL.mjs +4947 -0
  53. package/dist/chunk-XG2X7AEA.mjs +175 -0
  54. package/dist/chunk-XG7Z23NQ.mjs +148 -0
  55. package/dist/chunk-XWZAOCQ7.mjs +2607 -0
  56. package/dist/chunk-Y6MA7ULW.mjs +148 -0
  57. package/dist/chunk-YMS7Q7LG.mjs +214 -0
  58. package/dist/chunk-ZA37XTGA.mjs +175 -0
  59. package/dist/cli/index.js +1616 -366
  60. package/dist/cli/index.mjs +8 -8
  61. package/dist/codemod/cli.mjs +1 -1
  62. package/dist/codemod/index.mjs +1 -1
  63. package/dist/dev-server-RmGHIntF.d.mts +113 -0
  64. package/dist/dev-server-RmGHIntF.d.ts +113 -0
  65. package/dist/dev-server.d.mts +1 -1
  66. package/dist/dev-server.d.ts +1 -1
  67. package/dist/dev-server.js +982 -53
  68. package/dist/dev-server.mjs +5 -5
  69. package/dist/envelope.js +113 -6
  70. package/dist/envelope.mjs +3 -3
  71. package/dist/index.d.mts +5 -1
  72. package/dist/index.d.ts +5 -1
  73. package/dist/index.js +992 -63
  74. package/dist/index.mjs +8 -8
  75. package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
  76. package/dist/init-EHO4VQ22.mjs +369 -0
  77. package/dist/init-UC3FWPIW.mjs +367 -0
  78. package/dist/init-UNSMVKIK.mjs +366 -0
  79. package/dist/init-UNV5XIDE.mjs +367 -0
  80. package/dist/project-compiler-2P4N4DR7.mjs +10 -0
  81. package/dist/project-compiler-D2LCC27O.mjs +10 -0
  82. package/dist/project-compiler-EJ3GANJE.mjs +10 -0
  83. package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
  84. package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
  85. package/dist/project-compiler-VWNNCHGO.mjs +10 -0
  86. package/dist/project-compiler-XVAAU4C5.mjs +10 -0
  87. package/dist/project-compiler-YES5FGMD.mjs +10 -0
  88. package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
  89. package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
  90. package/dist/project-decompiler-VLPR22QF.mjs +7 -0
  91. package/dist/pull-FUS5QYZS.mjs +109 -0
  92. package/dist/pull-LD5ENLGY.mjs +109 -0
  93. package/dist/testing/index.js +113 -6
  94. package/dist/testing/index.mjs +2 -2
  95. package/dist/vite/index.js +113 -6
  96. package/dist/vite/index.mjs +3 -3
  97. package/examples/uber-app/app/admin/fleet.tsx +19 -19
  98. package/package.json +4 -3
  99. package/compile-blueprint-chat.mjs +0 -99
  100. package/compile-blueprint-glass-console.mjs +0 -98
  101. package/compile-chat-defs.mjs +0 -92
  102. package/examples/uber-app/tests/payment.test.tsx +0 -129
  103. package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
  104. package/package.json.backup +0 -86
  105. package/scripts/decompile.ts +0 -226
  106. package/scripts/seed-auth.ts +0 -267
  107. package/scripts/seed-uber.ts +0 -248
  108. package/scripts/validate-uber.ts +0 -119
  109. package/seed-blueprint-chat.mjs +0 -444
  110. package/seed-blueprint-glass-console.mjs +0 -445
  111. package/seed-compiled.mjs +0 -318
  112. package/src/RoundTripValidator.ts +0 -400
  113. package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
  114. package/src/__tests__/auth-module-compilation.test.ts +0 -247
  115. package/src/__tests__/auth-template-compilation.test.ts +0 -589
  116. package/src/__tests__/change-extractor.test.ts +0 -142
  117. package/src/__tests__/cli-pull.test.ts +0 -73
  118. package/src/__tests__/cli-test.test.ts +0 -72
  119. package/src/__tests__/component-extractor.test.ts +0 -331
  120. package/src/__tests__/context-extractor.test.ts +0 -145
  121. package/src/__tests__/decompiler.test.ts +0 -718
  122. package/src/__tests__/define-blueprint.test.ts +0 -133
  123. package/src/__tests__/definition-validator.test.ts +0 -519
  124. package/src/__tests__/during-extractor.test.ts +0 -152
  125. package/src/__tests__/effect-extractor.test.ts +0 -107
  126. package/src/__tests__/event-emission.test.ts +0 -127
  127. package/src/__tests__/examples.test.ts +0 -236
  128. package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
  129. package/src/__tests__/golden-suite.test.ts +0 -403
  130. package/src/__tests__/grammar-island-extractor.test.ts +0 -289
  131. package/src/__tests__/instance-key.test.ts +0 -82
  132. package/src/__tests__/ir-migration.test.ts +0 -255
  133. package/src/__tests__/lock-file.test.ts +0 -117
  134. package/src/__tests__/model-extractor.test.ts +0 -195
  135. package/src/__tests__/model-field-acl.test.ts +0 -237
  136. package/src/__tests__/model-hooks.test.ts +0 -130
  137. package/src/__tests__/model-ref-resolution.test.ts +0 -268
  138. package/src/__tests__/model-roundtrip.test.ts +0 -502
  139. package/src/__tests__/model-runtime.test.ts +0 -112
  140. package/src/__tests__/model-transitions.test.ts +0 -183
  141. package/src/__tests__/nrt-action-trace.test.ts +0 -391
  142. package/src/__tests__/pipeline-hardening.test.ts +0 -413
  143. package/src/__tests__/project-compiler.test.ts +0 -546
  144. package/src/__tests__/project-decompiler.test.ts +0 -343
  145. package/src/__tests__/query-compilation.test.ts +0 -145
  146. package/src/__tests__/round-trip/PLAN.md +0 -158
  147. package/src/__tests__/round-trip/README.md +0 -52
  148. package/src/__tests__/round-trip/RESULTS.md +0 -86
  149. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
  150. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
  151. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
  152. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
  153. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
  154. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
  155. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
  156. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
  157. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
  158. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
  159. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
  160. package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
  161. package/src/__tests__/round-trip-ir.test.ts +0 -300
  162. package/src/__tests__/round-trip.test.ts +0 -1212
  163. package/src/__tests__/route-merging.test.ts +0 -372
  164. package/src/__tests__/router-composition.test.ts +0 -489
  165. package/src/__tests__/router-extractor.test.ts +0 -176
  166. package/src/__tests__/server-action-extractor.test.ts +0 -128
  167. package/src/__tests__/smart-type-inference.test.ts +0 -365
  168. package/src/__tests__/source-envelope.test.ts +0 -284
  169. package/src/__tests__/source-fidelity.test.ts +0 -516
  170. package/src/__tests__/state-extractor.test.ts +0 -115
  171. package/src/__tests__/strict-mode.test.ts +0 -227
  172. package/src/__tests__/transition-effect-extractor.test.ts +0 -119
  173. package/src/__tests__/transition-extractor.test.ts +0 -68
  174. package/src/__tests__/ts-to-expression.test.ts +0 -462
  175. package/src/__tests__/type-generator.test.ts +0 -201
  176. package/src/__tests__/uber-validation.test.ts +0 -502
  177. package/src/action-compiler.ts +0 -361
  178. package/src/babel/emitters/experience-transform.ts +0 -199
  179. package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
  180. package/src/babel/emitters/pure-form-emitter.ts +0 -1023
  181. package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
  182. package/src/babel/extractors/change-extractor.ts +0 -199
  183. package/src/babel/extractors/component-extractor.ts +0 -907
  184. package/src/babel/extractors/computed-extractor.ts +0 -262
  185. package/src/babel/extractors/context-extractor.ts +0 -277
  186. package/src/babel/extractors/during-extractor.ts +0 -295
  187. package/src/babel/extractors/effect-extractor.ts +0 -340
  188. package/src/babel/extractors/event-extractor.ts +0 -235
  189. package/src/babel/extractors/grammar-island-extractor.ts +0 -302
  190. package/src/babel/extractors/model-extractor.ts +0 -1018
  191. package/src/babel/extractors/router-extractor.ts +0 -303
  192. package/src/babel/extractors/server-action-extractor.ts +0 -173
  193. package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
  194. package/src/babel/extractors/server-state-extractor.ts +0 -88
  195. package/src/babel/extractors/state-extractor.ts +0 -214
  196. package/src/babel/extractors/transition-effect-extractor.ts +0 -176
  197. package/src/babel/extractors/transition-extractor.ts +0 -143
  198. package/src/babel/index.ts +0 -24
  199. package/src/babel/transpilers/ts-to-expression.ts +0 -674
  200. package/src/babel/visitor.ts +0 -807
  201. package/src/cli/auth.ts +0 -255
  202. package/src/cli/build.ts +0 -288
  203. package/src/cli/deploy.ts +0 -206
  204. package/src/cli/index.ts +0 -328
  205. package/src/cli/installer.ts +0 -261
  206. package/src/cli/lock-file.ts +0 -94
  207. package/src/cli/mmrc.ts +0 -22
  208. package/src/cli/pull.ts +0 -172
  209. package/src/cli/registry-client.ts +0 -175
  210. package/src/cli/test.ts +0 -397
  211. package/src/cli/type-generator.ts +0 -243
  212. package/src/codemod/__tests__/forward.test.ts +0 -239
  213. package/src/codemod/__tests__/reverse.test.ts +0 -145
  214. package/src/codemod/__tests__/round-trip.test.ts +0 -137
  215. package/src/codemod/annotation.ts +0 -97
  216. package/src/codemod/classify.ts +0 -197
  217. package/src/codemod/cli.ts +0 -207
  218. package/src/codemod/control-flow.ts +0 -409
  219. package/src/codemod/forward.ts +0 -244
  220. package/src/codemod/import-manager.ts +0 -171
  221. package/src/codemod/index.ts +0 -120
  222. package/src/codemod/reverse.ts +0 -197
  223. package/src/codemod/rules.ts +0 -174
  224. package/src/codemod/state-transform.ts +0 -126
  225. package/src/decompiler/ast-builder.ts +0 -538
  226. package/src/decompiler/config-generator.ts +0 -151
  227. package/src/decompiler/index.ts +0 -315
  228. package/src/decompiler/project-decompiler.ts +0 -1776
  229. package/src/decompiler/project.ts +0 -862
  230. package/src/decompiler/split-strategy.ts +0 -140
  231. package/src/decompiler/state-emitter.ts +0 -1053
  232. package/src/decompiler/sx-emitter.ts +0 -318
  233. package/src/decompiler/workspace-hydrator.ts +0 -189
  234. package/src/dev-server.ts +0 -238
  235. package/src/envelope/fs-tree.ts +0 -217
  236. package/src/envelope/source-envelope.ts +0 -264
  237. package/src/envelope.ts +0 -315
  238. package/src/incremental-compiler.ts +0 -401
  239. package/src/index.ts +0 -99
  240. package/src/model-compiler.ts +0 -277
  241. package/src/project-compiler.ts +0 -1629
  242. package/src/route-extractor.ts +0 -333
  243. package/src/testing/index.ts +0 -32
  244. package/src/testing/snapshot.ts +0 -252
  245. package/src/testing/test-utils.ts +0 -226
  246. package/src/types.ts +0 -68
  247. package/src/vite/index.ts +0 -288
  248. package/test-compile.mjs +0 -142
  249. package/tsconfig.json +0 -25
  250. package/tsup.config.ts +0 -23
  251. package/vitest.config.ts +0 -9
@@ -1,907 +0,0 @@
1
- /**
2
- * Component Extractor — extracts JSX return value into IRExperienceNode tree.
3
- *
4
- * Handles:
5
- * - JSX elements with identifier and member expression names
6
- * - Fragments (anonymous containers)
7
- * - Text content as Text nodes
8
- * - Expression children ({count}, {status})
9
- * - Conditional rendering ({show && <X/>}, {cond ? <A/> : <B/>})
10
- * - List rendering ({items.map(i => <Item/>)})
11
- * - Spread attributes
12
- * - visible_when, data-slot special attributes
13
- */
14
-
15
- import type { NodePath } from '@babel/traverse';
16
- import * as t from '@babel/types';
17
- import type { IRExperienceNode, IRFieldDefinition } from '@mindmatrix/player-core';
18
- import type { CompilerState } from '../../types';
19
- import { transpileExpression } from '../transpilers/ts-to-expression';
20
-
21
- /**
22
- * Per-compilation node ID counter. Reset via resetNodeIdCounter().
23
- */
24
- let nodeIdCounter = 0;
25
-
26
- /**
27
- * Maps camelCase useState field names → snake_case names for $local bindings.
28
- * e.g. "sidebarOpen" → "sidebar_open"
29
- */
30
- let localFieldMap: Map<string, string> = new Map();
31
-
32
- /**
33
- * Maps setter function names → snake_case field names.
34
- * e.g. "setSidebarOpen" → "sidebar_open"
35
- */
36
- let setterToFieldMap: Map<string, string> = new Map();
37
-
38
- /**
39
- * Maps derived/computed variable names → their AST expressions.
40
- * e.g. "hasContent" → BinaryExpression(text.trim().length > 0 || files.length > 0)
41
- * When generateExpression encounters these, it inlines the expression
42
- * (recursively resolving $local references).
43
- */
44
- let derivedVarMap: Map<string, t.Expression> = new Map();
45
-
46
- /**
47
- * Converts camelCase to snake_case.
48
- */
49
- function toSnakeCase(str: string): string {
50
- return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
51
- }
52
-
53
- /**
54
- * Sentinel value for tryStaticEval failure (distinguishes from `undefined` as a valid result).
55
- */
56
- const STATIC_EVAL_FAIL = Symbol('STATIC_EVAL_FAIL');
57
-
58
- /**
59
- * Attempts to statically evaluate an AST expression into a plain JS value.
60
- * Returns the value for pure literals (arrays, objects, strings, numbers, booleans, null, undefined).
61
- * Returns STATIC_EVAL_FAIL if the expression contains dynamic references.
62
- */
63
- function tryStaticEval(node: t.Expression): unknown {
64
- if (t.isStringLiteral(node)) return node.value;
65
- if (t.isNumericLiteral(node)) return node.value;
66
- if (t.isBooleanLiteral(node)) return node.value;
67
- if (t.isNullLiteral(node)) return null;
68
- if (t.isIdentifier(node) && node.name === 'undefined') return undefined;
69
-
70
- if (t.isUnaryExpression(node) && node.operator === '-' && t.isNumericLiteral(node.argument)) {
71
- return -node.argument.value;
72
- }
73
-
74
- if (t.isArrayExpression(node)) {
75
- const result: unknown[] = [];
76
- for (const el of node.elements) {
77
- if (el === null) { result.push(null); continue; }
78
- if (t.isSpreadElement(el)) return STATIC_EVAL_FAIL;
79
- const val = tryStaticEval(el);
80
- if (val === STATIC_EVAL_FAIL) return STATIC_EVAL_FAIL;
81
- result.push(val);
82
- }
83
- return result;
84
- }
85
-
86
- if (t.isObjectExpression(node)) {
87
- const result: Record<string, unknown> = {};
88
- for (const prop of node.properties) {
89
- if (t.isSpreadElement(prop) || t.isObjectMethod(prop)) return STATIC_EVAL_FAIL;
90
- if (!t.isObjectProperty(prop)) return STATIC_EVAL_FAIL;
91
- let key: string;
92
- if (t.isIdentifier(prop.key)) key = prop.key.name;
93
- else if (t.isStringLiteral(prop.key)) key = prop.key.value;
94
- else if (t.isNumericLiteral(prop.key)) key = String(prop.key.value);
95
- else return STATIC_EVAL_FAIL;
96
- if (!t.isExpression(prop.value)) return STATIC_EVAL_FAIL;
97
- const val = tryStaticEval(prop.value);
98
- if (val === STATIC_EVAL_FAIL) return STATIC_EVAL_FAIL;
99
- result[key] = val;
100
- }
101
- return result;
102
- }
103
-
104
- // Template literal with no expressions: `hello world`
105
- if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
106
- return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
107
- }
108
-
109
- return STATIC_EVAL_FAIL;
110
- }
111
-
112
- /**
113
- * Initializes field maps from extracted useState fields.
114
- * Must be called before JSX extraction.
115
- */
116
- function initLocalFieldMaps(fields: IRFieldDefinition[]): void {
117
- localFieldMap.clear();
118
- setterToFieldMap.clear();
119
- for (const field of fields) {
120
- const camelName = field.name;
121
- const snakeName = toSnakeCase(camelName);
122
- localFieldMap.set(camelName, snakeName);
123
- // Build setter name: "sidebarOpen" → "setSidebarOpen"
124
- const setterName = 'set' + camelName.charAt(0).toUpperCase() + camelName.slice(1);
125
- setterToFieldMap.set(setterName, snakeName);
126
- }
127
- }
128
-
129
- /**
130
- * Register a derived variable (const declaration) for inline expansion.
131
- * When the variable is referenced in JSX expressions, its initializer
132
- * expression is inlined with $local substitutions applied.
133
- */
134
- export function registerDerivedVar(name: string, init: t.Expression): void {
135
- derivedVarMap.set(name, init);
136
- }
137
-
138
- /**
139
- * Resets the node ID counter. MUST be called between file compilations.
140
- */
141
- export function resetNodeIdCounter(): void {
142
- nodeIdCounter = 0;
143
- derivedVarMap.clear();
144
- }
145
-
146
- /**
147
- * Converts JSX element to IRExperienceNode.
148
- */
149
- function jsxToExperienceNode(node: t.JSXElement | t.JSXFragment): IRExperienceNode {
150
- if (t.isJSXFragment(node)) {
151
- return {
152
- id: `fragment_${++nodeIdCounter}`,
153
- children: extractChildren(node.children),
154
- };
155
- }
156
-
157
- const element = node.openingElement;
158
- const componentName = resolveComponentName(element.name);
159
- const id = generateNodeId(componentName);
160
- const config: Record<string, unknown> = {};
161
- const bindings: Record<string, string> = {};
162
- let visibleWhen: string | undefined;
163
- let layout: string | undefined;
164
- let slot: string | undefined;
165
- let className: string | undefined;
166
- let displayName: string | undefined;
167
-
168
- // Infer layout from component type
169
- const layoutMap: Record<string, string> = {
170
- Stack: 'stack', Row: 'row', Grid: 'grid', Tabs: 'tabs', Column: 'column',
171
- };
172
- if (layoutMap[componentName]) {
173
- layout = layoutMap[componentName];
174
- }
175
-
176
- // Extract attributes
177
- for (const attr of element.attributes) {
178
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
179
- const attrName = attr.name.name;
180
- const attrValue = attr.value;
181
-
182
- if (attrName === 'visible_when' && t.isStringLiteral(attrValue)) {
183
- visibleWhen = attrValue.value;
184
- } else if (attrName === 'data-slot' && t.isStringLiteral(attrValue)) {
185
- slot = attrValue.value;
186
- } else if (attrName === 'className' && t.isStringLiteral(attrValue)) {
187
- className = attrValue.value;
188
- } else if (attrName === 'displayName' && t.isStringLiteral(attrValue)) {
189
- displayName = attrValue.value;
190
- } else if (t.isJSXExpressionContainer(attrValue)) {
191
- const expr = attrValue.expression;
192
- if (t.isJSXEmptyExpression(expr)) continue;
193
- // Literals go to config, dynamic expressions go to bindings
194
- if (t.isNumericLiteral(expr)) {
195
- config[attrName] = expr.value;
196
- } else if (t.isBooleanLiteral(expr)) {
197
- config[attrName] = expr.value;
198
- } else if (t.isStringLiteral(expr)) {
199
- // String literals that look like binding expressions (containing $instance,
200
- // $local, or function calls) should be stored as bindings for round-trip
201
- // stability. Plain strings go to config.
202
- if (expr.value.includes('$instance') || expr.value.includes('$local') || /^[a-zA-Z_]+\(/.test(expr.value)) {
203
- bindings[attrName] = expr.value;
204
- } else {
205
- config[attrName] = expr.value;
206
- }
207
- } else if (t.isIdentifier(expr)) {
208
- const snakeName = localFieldMap.get(expr.name);
209
- bindings[attrName] = snakeName ? `$local.${snakeName}` : `$instance.${expr.name}`;
210
- } else if (
211
- isEventHandlerProp(attrName) &&
212
- (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr))
213
- ) {
214
- // Decompose event handlers into action sequences
215
- const decomposed = decomposeHandlerToSeq(expr);
216
- if (decomposed) {
217
- bindings[attrName] = decomposed;
218
- } else {
219
- // Fallback: wrap raw JS in $expr() so IR consumers can identify
220
- // opaque JavaScript expressions that don't map to mm-compute syntax.
221
- // The binding resolver strips $expr() at evaluation time.
222
- bindings[attrName] = `$expr(${generateExpression(expr)})`;
223
- }
224
- } else if (t.isExpression(expr)) {
225
- // Try to statically evaluate pure-literal expressions (arrays/objects of primitives)
226
- // into config values instead of putting them in bindings as strings
227
- const staticVal = tryStaticEval(expr);
228
- if (staticVal !== STATIC_EVAL_FAIL) {
229
- config[attrName] = staticVal;
230
- } else {
231
- bindings[attrName] = generateExpression(expr);
232
- }
233
- }
234
- } else if (t.isStringLiteral(attrValue)) {
235
- config[attrName] = attrValue.value;
236
- } else if (attrValue === null) {
237
- // Boolean attribute: <Button disabled />
238
- config[attrName] = true;
239
- }
240
- } else if (t.isJSXSpreadAttribute(attr)) {
241
- // Spread attributes: {...props} → store the identifier in bindings
242
- if (t.isIdentifier(attr.argument)) {
243
- bindings['...' + attr.argument.name] = `$instance.${attr.argument.name}`;
244
- }
245
- }
246
- }
247
-
248
- const children = extractChildren(node.children);
249
-
250
- const experienceNode: IRExperienceNode = {
251
- id,
252
- ...(displayName && { displayName }),
253
- component: componentName,
254
- ...(slot && { slot }),
255
- ...(layout && { layout }),
256
- ...(className && { className }),
257
- ...(Object.keys(bindings).length > 0 && { bindings }),
258
- ...(Object.keys(config).length > 0 && { config }),
259
- ...(visibleWhen && { visible_when: visibleWhen }),
260
- ...(children.length > 0 && { children }),
261
- };
262
-
263
- return experienceNode;
264
- }
265
-
266
- /**
267
- * Resolves component name from JSX element name node.
268
- * Handles JSXIdentifier and JSXMemberExpression.
269
- */
270
- function resolveComponentName(name: t.JSXIdentifier | t.JSXMemberExpression | t.JSXNamespacedName): string {
271
- if (t.isJSXIdentifier(name)) return name.name;
272
- if (t.isJSXMemberExpression(name)) {
273
- const obj = resolveComponentName(name.object);
274
- return `${obj}.${name.property.name}`;
275
- }
276
- if (t.isJSXNamespacedName(name)) {
277
- return `${name.namespace.name}:${name.name.name}`;
278
- }
279
- return 'div';
280
- }
281
-
282
- /**
283
- * Extracts children from JSX, handling text, expressions, conditionals, and lists.
284
- */
285
- function extractChildren(
286
- children: Array<t.JSXText | t.JSXExpressionContainer | t.JSXElement | t.JSXFragment | t.JSXSpreadChild>
287
- ): IRExperienceNode[] {
288
- const nodes: IRExperienceNode[] = [];
289
-
290
- for (const child of children) {
291
- if (t.isJSXElement(child)) {
292
- nodes.push(jsxToExperienceNode(child));
293
- } else if (t.isJSXFragment(child)) {
294
- nodes.push(jsxToExperienceNode(child));
295
- } else if (t.isJSXText(child)) {
296
- const text = child.value.trim();
297
- if (text) {
298
- nodes.push({
299
- id: `text_${++nodeIdCounter}`,
300
- component: 'Text',
301
- config: { value: text },
302
- });
303
- }
304
- } else if (t.isJSXExpressionContainer(child)) {
305
- const expr = child.expression;
306
- if (t.isJSXEmptyExpression(expr)) continue;
307
-
308
- // Conditional: {cond ? <A/> : <B/>}
309
- if (t.isConditionalExpression(expr)) {
310
- const condExpr = generateExpression(expr.test);
311
- if (t.isJSXElement(expr.consequent) || t.isJSXFragment(expr.consequent)) {
312
- const consequent = jsxToExperienceNode(expr.consequent as t.JSXElement | t.JSXFragment);
313
- consequent.visible_when = condExpr;
314
- nodes.push(consequent);
315
- }
316
- if (t.isJSXElement(expr.alternate) || t.isJSXFragment(expr.alternate)) {
317
- const alternate = jsxToExperienceNode(expr.alternate as t.JSXElement | t.JSXFragment);
318
- alternate.visible_when = `not(${condExpr})`;
319
- nodes.push(alternate);
320
- }
321
- }
322
- // Logical AND: {show && <Component/>}
323
- else if (t.isLogicalExpression(expr) && expr.operator === '&&') {
324
- const condExpr = generateExpression(expr.left);
325
- if (t.isJSXElement(expr.right) || t.isJSXFragment(expr.right)) {
326
- const element = jsxToExperienceNode(expr.right as t.JSXElement | t.JSXFragment);
327
- element.visible_when = condExpr;
328
- nodes.push(element);
329
- }
330
- }
331
- // List: {items.map(item => <Item key={...} />)}
332
- else if (
333
- t.isCallExpression(expr) &&
334
- t.isMemberExpression(expr.callee) &&
335
- t.isIdentifier(expr.callee.property) &&
336
- expr.callee.property.name === 'map' &&
337
- expr.arguments.length > 0
338
- ) {
339
- const listSource = generateExpression(expr.callee.object as t.Expression);
340
- const mapFn = expr.arguments[0];
341
- let itemTemplate: IRExperienceNode | null = null;
342
- let itemAlias = 'item';
343
-
344
- if (t.isArrowFunctionExpression(mapFn) || t.isFunctionExpression(mapFn)) {
345
- // Get the item parameter name
346
- if (mapFn.params.length > 0 && t.isIdentifier(mapFn.params[0])) {
347
- itemAlias = mapFn.params[0].name;
348
- }
349
- // Get the returned JSX
350
- const body = mapFn.body;
351
- if (t.isJSXElement(body) || t.isJSXFragment(body)) {
352
- itemTemplate = jsxToExperienceNode(body as t.JSXElement | t.JSXFragment);
353
- } else if (t.isBlockStatement(body)) {
354
- for (const stmt of body.body) {
355
- if (t.isReturnStatement(stmt) && stmt.argument) {
356
- if (t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument)) {
357
- itemTemplate = jsxToExperienceNode(stmt.argument as t.JSXElement | t.JSXFragment);
358
- }
359
- break;
360
- }
361
- }
362
- }
363
- }
364
-
365
- nodes.push({
366
- id: `each_${++nodeIdCounter}`,
367
- component: 'Each',
368
- bindings: { items: listSource.startsWith('$') ? listSource : `$instance.${listSource}` },
369
- config: { as: itemAlias },
370
- ...(itemTemplate ? { children: [itemTemplate] } : {}),
371
- });
372
- }
373
- // Render callback: {(item) => <Component/>} or {(item, index) => { return <Component/>; }}
374
- // This is the children-as-function pattern used by Each, Show, etc.
375
- else if (
376
- (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) &&
377
- extractJSXFromCallback(expr)
378
- ) {
379
- const jsxNode = extractJSXFromCallback(expr)!;
380
- nodes.push(jsxToExperienceNode(jsxNode));
381
- }
382
- // String literal in expression: {' '}, {"hello"} → static Text config
383
- else if (t.isStringLiteral(expr)) {
384
- nodes.push({
385
- id: `text_${++nodeIdCounter}`,
386
- component: 'Text',
387
- config: { value: expr.value },
388
- });
389
- }
390
- // Simple expression: {count}, {status}
391
- else if (t.isIdentifier(expr)) {
392
- const snakeName = localFieldMap.get(expr.name);
393
- const bindingPath = snakeName ? `$local.${snakeName}` : `$instance.${expr.name}`;
394
- nodes.push({
395
- id: `text_${++nodeIdCounter}`,
396
- component: 'Text',
397
- bindings: { value: bindingPath },
398
- });
399
- }
400
- // Complex expression: {a + b}, {fn(x)}
401
- else if (t.isExpression(expr)) {
402
- const exprStr = generateExpression(expr);
403
- if (exprStr !== '[Expression]') {
404
- nodes.push({
405
- id: `text_${++nodeIdCounter}`,
406
- component: 'Text',
407
- bindings: { value: exprStr },
408
- });
409
- }
410
- }
411
- }
412
- }
413
-
414
- return nodes;
415
- }
416
-
417
- /**
418
- * Generates a unique node ID from component name.
419
- */
420
- function generateNodeId(componentName: string): string {
421
- const id = componentName
422
- .replace(/\./g, '-')
423
- .replace(/([A-Z])/g, '-$1')
424
- .toLowerCase()
425
- .replace(/^-/, '');
426
- return `${id}_${++nodeIdCounter}`;
427
- }
428
-
429
- /**
430
- * Generates expression string from AST node.
431
- *
432
- * The output must be valid JS that the binding resolver can evaluate
433
- * via `new Function(...)`. All $-prefix paths are resolved at runtime
434
- * from the binding context ($instance, $local, $fn, $action, etc.).
435
- */
436
- function generateExpression(node: t.Expression): string {
437
- if (t.isIdentifier(node)) {
438
- // Map useState fields to $local.snake_case
439
- const snakeName = localFieldMap.get(node.name);
440
- if (snakeName) return `$local.${snakeName}`;
441
- // Inline derived variables: expand to their defining expression
442
- const derivedInit = derivedVarMap.get(node.name);
443
- if (derivedInit) return `(${generateExpression(derivedInit)})`;
444
- return node.name;
445
- }
446
- if (t.isStringLiteral(node)) return `"${node.value.replace(/"/g, '\\"')}"`;
447
- if (t.isNumericLiteral(node)) return String(node.value);
448
- if (t.isBooleanLiteral(node)) return String(node.value);
449
- if (t.isNullLiteral(node)) return 'null';
450
-
451
- // Member expression: a.b, a?.b, a[0], a["key"]
452
- if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
453
- const obj = t.isExpression(node.object) ? generateExpression(node.object) : '[object]';
454
- const optional = (node as any).optional ? '?.' : '.';
455
- if (node.computed) {
456
- const prop = t.isExpression(node.property) ? generateExpression(node.property as t.Expression) : '0';
457
- return `${obj}[${prop}]`;
458
- }
459
- const prop = t.isIdentifier(node.property) ? node.property.name : '[property]';
460
- return `${obj}${optional}${prop}`;
461
- }
462
-
463
- // Binary: a + b, a === b, a !== b, etc.
464
- if (t.isBinaryExpression(node)) {
465
- return `(${generateExpression(node.left as t.Expression)} ${node.operator} ${generateExpression(node.right as t.Expression)})`;
466
- }
467
-
468
- // Logical: a && b, a || b, a ?? b
469
- if (t.isLogicalExpression(node)) {
470
- return `(${generateExpression(node.left)} ${node.operator} ${generateExpression(node.right)})`;
471
- }
472
-
473
- // Conditional (ternary): a ? b : c
474
- if (t.isConditionalExpression(node)) {
475
- return `(${generateExpression(node.test)} ? ${generateExpression(node.consequent)} : ${generateExpression(node.alternate)})`;
476
- }
477
-
478
- // Unary: !a, -a, typeof a
479
- if (t.isUnaryExpression(node) && node.prefix) {
480
- const space = node.operator === 'typeof' || node.operator === 'void' || node.operator === 'delete' ? ' ' : '';
481
- return `${node.operator}${space}${generateExpression(node.argument)}`;
482
- }
483
-
484
- // Call expression: fn(args) or obj.method(args)
485
- if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
486
- if (t.isIdentifier(node.callee)) {
487
- // Detect useState setter calls: setSomething(value) → $action.setLocal("field")(value)
488
- const setterFieldName = setterToFieldMap.get(node.callee.name);
489
- if (setterFieldName) {
490
- if (node.arguments.length >= 1) {
491
- const arg = node.arguments[0];
492
- // Functional update: setSomething(prev => expr) → $action.setLocal("field")(expr with prev→$local.field)
493
- if (
494
- (t.isArrowFunctionExpression(arg) || t.isFunctionExpression(arg)) &&
495
- arg.params.length >= 1 &&
496
- t.isIdentifier(arg.params[0])
497
- ) {
498
- const paramName = arg.params[0].name;
499
- const bodyExpr = t.isExpression(arg.body) ? generateExpression(arg.body) : 'undefined';
500
- // Replace the callback param with the actual $local field reference
501
- const resolved = bodyExpr.replace(
502
- new RegExp(`(?<![.$\\w])${paramName}(?![\\w])`, 'g'),
503
- `$local.${setterFieldName}`,
504
- );
505
- return `$action.setLocal("${setterFieldName}")(${resolved})`;
506
- }
507
- // Direct value: setSomething(value) → $action.setLocal("field")(value)
508
- const argStr = t.isExpression(arg) ? generateExpression(arg) : 'undefined';
509
- return `$action.setLocal("${setterFieldName}")(${argStr})`;
510
- }
511
- return `$action.setLocal("${setterFieldName}")`;
512
- }
513
- const args = node.arguments.map((a) => t.isExpression(a) ? generateExpression(a) : t.isSpreadElement(a) ? `...${generateExpression(a.argument)}` : 'undefined').join(', ');
514
- return `${node.callee.name}(${args})`;
515
- }
516
- if (t.isMemberExpression(node.callee) || t.isOptionalMemberExpression(node.callee)) {
517
- // Try mm-compute transpilation for recognized method patterns
518
- // (Math.*, JSON.*, Date.*, string/array methods)
519
- const mmResult = transpileExpression(node, {
520
- localFieldMap,
521
- derivedVarMap,
522
- setterToFieldMap,
523
- });
524
- if (mmResult.pure) return mmResult.expression;
525
-
526
- // Fallback: raw JS method call syntax
527
- const obj = generateExpression(node.callee.object as t.Expression);
528
- const optional = (node.callee as any).optional ? '?.' : '.';
529
- const prop = t.isIdentifier(node.callee.property) ? node.callee.property.name : '[method]';
530
- const args = node.arguments.map((a) => t.isExpression(a) ? generateExpression(a) : t.isSpreadElement(a) ? `...${generateExpression(a.argument)}` : 'undefined').join(', ');
531
- return `${obj}${optional}${prop}(${args})`;
532
- }
533
- // Callee is a more complex expression (e.g., (fn || fallback)(args))
534
- if (t.isExpression(node.callee)) {
535
- const callee = generateExpression(node.callee);
536
- const args = node.arguments.map((a) => t.isExpression(a) ? generateExpression(a) : 'undefined').join(', ');
537
- return `(${callee})(${args})`;
538
- }
539
- }
540
-
541
- // Template literal: `hello ${name}`
542
- if (t.isTemplateLiteral(node)) {
543
- const parts = node.quasis.map((q, i) => {
544
- const raw = q.value.raw;
545
- if (i < node.expressions.length) {
546
- return raw + '${' + generateExpression(node.expressions[i] as t.Expression) + '}';
547
- }
548
- return raw;
549
- }).join('');
550
- return '`' + parts + '`';
551
- }
552
-
553
- // Arrow function: () => expr, (a) => expr, (a) => { ... }
554
- if (t.isArrowFunctionExpression(node)) {
555
- const params = node.params.map((p) => {
556
- if (t.isIdentifier(p)) return p.name;
557
- if (t.isRestElement(p) && t.isIdentifier(p.argument)) return `...${p.argument.name}`;
558
- if (t.isAssignmentPattern(p) && t.isIdentifier(p.left)) {
559
- return `${p.left.name} = ${t.isExpression(p.right) ? generateExpression(p.right) : 'undefined'}`;
560
- }
561
- return '_';
562
- }).join(', ');
563
- if (t.isExpression(node.body)) {
564
- return `(${params}) => ${generateExpression(node.body)}`;
565
- }
566
- // Block body — generate the statements
567
- if (t.isBlockStatement(node.body)) {
568
- const bodyStr = generateBlockStatement(node.body);
569
- return `(${params}) => { ${bodyStr} }`;
570
- }
571
- }
572
-
573
- // Array expression: [1, 2, 3]
574
- if (t.isArrayExpression(node)) {
575
- const elements = node.elements.map((el) => {
576
- if (el === null) return 'undefined';
577
- if (t.isSpreadElement(el)) return `...${generateExpression(el.argument)}`;
578
- return t.isExpression(el) ? generateExpression(el) : 'undefined';
579
- }).join(', ');
580
- return `[${elements}]`;
581
- }
582
-
583
- // Object expression: { key: value }
584
- if (t.isObjectExpression(node)) {
585
- const props = node.properties.map((prop) => {
586
- if (t.isSpreadElement(prop)) return `...${generateExpression(prop.argument)}`;
587
- if (t.isObjectProperty(prop)) {
588
- const key = t.isIdentifier(prop.key) ? prop.key.name
589
- : t.isStringLiteral(prop.key) ? `"${prop.key.value}"`
590
- : t.isNumericLiteral(prop.key) ? String(prop.key.value)
591
- : '[key]';
592
- if (prop.shorthand && t.isIdentifier(prop.value)) return key;
593
- const value = t.isExpression(prop.value) ? generateExpression(prop.value) : 'undefined';
594
- return `${key}: ${value}`;
595
- }
596
- if (t.isObjectMethod(prop)) {
597
- const key = t.isIdentifier(prop.key) ? prop.key.name : '[method]';
598
- const params = prop.params.map((p) => t.isIdentifier(p) ? p.name : '_').join(', ');
599
- const bodyStr = generateBlockStatement(prop.body);
600
- return `${key}(${params}) { ${bodyStr} }`;
601
- }
602
- return '';
603
- }).filter(Boolean).join(', ');
604
- return `{ ${props} }`;
605
- }
606
-
607
- // Parenthesized expression
608
- if (t.isParenthesizedExpression(node)) {
609
- return `(${generateExpression(node.expression)})`;
610
- }
611
-
612
- // TS type assertions: x as Type, x!
613
- if (t.isTSAsExpression(node)) return generateExpression(node.expression);
614
- if (t.isTSNonNullExpression(node)) return generateExpression(node.expression);
615
- if (t.isTSTypeAssertion(node)) return generateExpression(node.expression);
616
-
617
- // Sequence expression: (a, b, c) — returns last value
618
- if (t.isSequenceExpression(node)) {
619
- return `(${node.expressions.map(generateExpression).join(', ')})`;
620
- }
621
-
622
- // Assignment expression: a = b
623
- if (t.isAssignmentExpression(node)) {
624
- const left = t.isExpression(node.left) ? generateExpression(node.left as t.Expression) : '[target]';
625
- return `(${left} ${node.operator} ${generateExpression(node.right)})`;
626
- }
627
-
628
- // Update expression: a++, --b
629
- if (t.isUpdateExpression(node)) {
630
- const arg = generateExpression(node.argument);
631
- return node.prefix ? `${node.operator}${arg}` : `${arg}${node.operator}`;
632
- }
633
-
634
- // Tagged template: tag`string`
635
- if (t.isTaggedTemplateExpression(node)) {
636
- return generateExpression(node.quasi);
637
- }
638
-
639
- // Spread in non-array/object context (shouldn't happen but safety)
640
- if (t.isSpreadElement(node as any)) {
641
- return `...${generateExpression((node as any).argument)}`;
642
- }
643
-
644
- // Yield: yield expr (generators — unlikely in views but handle gracefully)
645
- if (t.isYieldExpression(node)) {
646
- return node.argument ? `yield ${generateExpression(node.argument)}` : 'yield';
647
- }
648
-
649
- // Await: await expr
650
- if (t.isAwaitExpression(node)) {
651
- return `await ${generateExpression(node.argument)}`;
652
- }
653
-
654
- // New expression: new Foo(args)
655
- if (t.isNewExpression(node)) {
656
- const callee = t.isExpression(node.callee) ? generateExpression(node.callee) : 'Object';
657
- const args = node.arguments.map((a) => t.isExpression(a) ? generateExpression(a) : 'undefined').join(', ');
658
- return `new ${callee}(${args})`;
659
- }
660
-
661
- // This expression
662
- if (t.isThisExpression(node)) return 'this';
663
-
664
- // Fallback — generate from source if possible using Babel's generator
665
- console.warn(`[component-extractor] Unhandled expression type: ${node.type}`);
666
- return '[Expression]';
667
- }
668
-
669
- /**
670
- * Generates a block statement body as a string.
671
- */
672
- function generateBlockStatement(block: t.BlockStatement): string {
673
- return block.body.map((stmt) => {
674
- if (t.isReturnStatement(stmt)) {
675
- return stmt.argument ? `return ${generateExpression(stmt.argument)};` : 'return;';
676
- }
677
- if (t.isExpressionStatement(stmt)) {
678
- return `${generateExpression(stmt.expression)};`;
679
- }
680
- if (t.isVariableDeclaration(stmt)) {
681
- const decls = stmt.declarations.map((d) => {
682
- const id = t.isIdentifier(d.id) ? d.id.name : '_';
683
- const init = d.init && t.isExpression(d.init) ? generateExpression(d.init) : 'undefined';
684
- return `${id} = ${init}`;
685
- }).join(', ');
686
- return `${stmt.kind} ${decls};`;
687
- }
688
- if (t.isIfStatement(stmt)) {
689
- const test = generateExpression(stmt.test);
690
- const consequent = t.isBlockStatement(stmt.consequent)
691
- ? `{ ${generateBlockStatement(stmt.consequent)} }`
692
- : t.isExpressionStatement(stmt.consequent)
693
- ? `{ ${generateExpression(stmt.consequent.expression)}; }`
694
- : '{}';
695
- const alternate = stmt.alternate
696
- ? t.isBlockStatement(stmt.alternate)
697
- ? ` else { ${generateBlockStatement(stmt.alternate)} }`
698
- : t.isIfStatement(stmt.alternate)
699
- ? ` else ${generateBlockStatement(t.blockStatement([stmt.alternate]))}`
700
- : ''
701
- : '';
702
- return `if (${test}) ${consequent}${alternate}`;
703
- }
704
- // Fallback for other statement types
705
- return '/* [Statement] */';
706
- }).join(' ');
707
- }
708
-
709
- /**
710
- * Extracts JSX element from a render callback function.
711
- * Handles: (item) => <JSX/>, (item) => (<JSX/>), (item) => { return <JSX/>; }
712
- */
713
- function extractJSXFromCallback(
714
- node: t.ArrowFunctionExpression | t.FunctionExpression
715
- ): t.JSXElement | t.JSXFragment | null {
716
- const body = node.body;
717
- if (t.isJSXElement(body) || t.isJSXFragment(body)) {
718
- return body as t.JSXElement | t.JSXFragment;
719
- }
720
- // Parenthesized: (item) => (<JSX/>)
721
- if (t.isParenthesizedExpression(body)) {
722
- const inner = body.expression;
723
- if (t.isJSXElement(inner) || t.isJSXFragment(inner)) {
724
- return inner as t.JSXElement | t.JSXFragment;
725
- }
726
- }
727
- // Block body: (item) => { return <JSX/>; }
728
- if (t.isBlockStatement(body)) {
729
- for (const stmt of body.body) {
730
- if (t.isReturnStatement(stmt) && stmt.argument) {
731
- if (t.isJSXElement(stmt.argument) || t.isJSXFragment(stmt.argument)) {
732
- return stmt.argument as t.JSXElement | t.JSXFragment;
733
- }
734
- // Parenthesized return: return (<JSX/>)
735
- if (t.isParenthesizedExpression(stmt.argument)) {
736
- const inner = stmt.argument.expression;
737
- if (t.isJSXElement(inner) || t.isJSXFragment(inner)) {
738
- return inner as t.JSXElement | t.JSXFragment;
739
- }
740
- }
741
- }
742
- }
743
- }
744
- return null;
745
- }
746
-
747
- // ============================================================================
748
- // EVENT HANDLER DECOMPOSITION
749
- // ============================================================================
750
-
751
- /**
752
- * Returns true for event handler prop names (onClick, onPress, onChange, etc.)
753
- */
754
- function isEventHandlerProp(name: string): boolean {
755
- return /^on[A-Z]/.test(name);
756
- }
757
-
758
- /**
759
- * Converts a single expression (statement body) into an atomic action string.
760
- * Returns null if the expression cannot be decomposed.
761
- */
762
- function expressionToActionAtom(expr: t.Expression): string | null {
763
- // Call expression: setter, transition, navigate, toast, etc.
764
- if (t.isCallExpression(expr) || t.isOptionalCallExpression(expr)) {
765
- // Direct setter: setFoo(value)
766
- if (t.isIdentifier(expr.callee)) {
767
- const setterFieldName = setterToFieldMap.get(expr.callee.name);
768
- if (setterFieldName && expr.arguments.length >= 1) {
769
- const arg = expr.arguments[0];
770
- // Functional update: setFoo(prev => expr)
771
- if (
772
- (t.isArrowFunctionExpression(arg) || t.isFunctionExpression(arg)) &&
773
- arg.params.length >= 1 &&
774
- t.isIdentifier(arg.params[0])
775
- ) {
776
- const paramName = arg.params[0].name;
777
- const bodyExpr = t.isExpression(arg.body) ? generateExpression(arg.body) : 'undefined';
778
- const resolved = bodyExpr.replace(
779
- new RegExp(`(?<![.$\\w])${paramName}(?![\\w])`, 'g'),
780
- `$local.${setterFieldName}`,
781
- );
782
- return `$action.setLocal("${setterFieldName}", ${resolved})`;
783
- }
784
- // Direct value: setFoo(value)
785
- const argStr = t.isExpression(arg) ? generateExpression(arg) : 'undefined';
786
- return `$action.setLocal("${setterFieldName}", ${argStr})`;
787
- }
788
- // Other function calls — not decomposable into framework actions
789
- return null;
790
- }
791
-
792
- // Member call: $action-like calls from chained setLocal etc.
793
- // e.g., $action.setLocal("key")(value) — already handled above via setter detection
794
- // For now, try to generate as expression and check if it's an $action call
795
- if (t.isMemberExpression(expr.callee) || t.isOptionalMemberExpression(expr.callee)) {
796
- const generated = generateExpression(expr);
797
- // If it looks like an $action call, wrap it as a 2-arg form
798
- if (generated.startsWith('$action.')) return generated;
799
- }
800
-
801
- return null;
802
- }
803
-
804
- return null;
805
- }
806
-
807
- /**
808
- * Converts a statement into an action atom string.
809
- * Returns null for unsupported statement types.
810
- */
811
- function statementToAction(stmt: t.Statement): string | null {
812
- // ExpressionStatement: the most common case
813
- if (t.isExpressionStatement(stmt)) {
814
- return expressionToActionAtom(stmt.expression);
815
- }
816
-
817
- // IfStatement: convert to $action.when(condition, thenAction, elseAction?)
818
- if (t.isIfStatement(stmt)) {
819
- const condition = generateExpression(stmt.test);
820
-
821
- // Consequent
822
- let thenAction: string | null = null;
823
- if (t.isBlockStatement(stmt.consequent)) {
824
- const actions = stmt.consequent.body.map(s => statementToAction(s)).filter(Boolean) as string[];
825
- if (actions.length === 0 || actions.length !== stmt.consequent.body.length) return null;
826
- thenAction = actions.length === 1 ? actions[0] : `$action.seq(${actions.join(', ')})`;
827
- } else if (t.isExpressionStatement(stmt.consequent)) {
828
- thenAction = expressionToActionAtom(stmt.consequent.expression);
829
- }
830
- if (!thenAction) return null;
831
-
832
- // Alternate (optional)
833
- let elseAction: string | null = null;
834
- if (stmt.alternate) {
835
- if (t.isBlockStatement(stmt.alternate)) {
836
- const actions = stmt.alternate.body.map(s => statementToAction(s)).filter(Boolean) as string[];
837
- if (actions.length !== stmt.alternate.body.length) return null;
838
- elseAction = actions.length === 1 ? actions[0] : `$action.seq(${actions.join(', ')})`;
839
- } else if (t.isIfStatement(stmt.alternate)) {
840
- elseAction = statementToAction(stmt.alternate);
841
- } else if (t.isExpressionStatement(stmt.alternate)) {
842
- elseAction = expressionToActionAtom(stmt.alternate.expression);
843
- }
844
- if (!elseAction) return null;
845
- }
846
-
847
- return elseAction
848
- ? `$action.when(${condition}, ${thenAction}, ${elseAction})`
849
- : `$action.when(${condition}, ${thenAction})`;
850
- }
851
-
852
- return null;
853
- }
854
-
855
- /**
856
- * Decomposes an arrow/function expression handler into a $action.seq() string.
857
- * For single-expression arrows, returns the single action atom.
858
- * Returns null if decomposition fails (fallback to generateExpression).
859
- */
860
- function decomposeHandlerToSeq(node: t.ArrowFunctionExpression | t.FunctionExpression): string | null {
861
- // Expression body: () => setFoo(value) — single action
862
- if (t.isArrowFunctionExpression(node) && t.isExpression(node.body)) {
863
- return expressionToActionAtom(node.body);
864
- }
865
-
866
- // Block body: () => { stmt1; stmt2; ... }
867
- const body = node.body;
868
- if (!t.isBlockStatement(body)) return null;
869
-
870
- const actions: string[] = [];
871
- for (const stmt of body.body) {
872
- const action = statementToAction(stmt);
873
- if (!action) return null; // Can't decompose — bail
874
- actions.push(action);
875
- }
876
-
877
- if (actions.length === 0) return null;
878
- if (actions.length === 1) return actions[0];
879
- return `$action.seq(${actions.join(', ')})`;
880
- }
881
-
882
- /**
883
- * Main extractor function called from the visitor.
884
- */
885
- export function extractComponents(path: NodePath<t.ReturnStatement>, state: any): void {
886
- const arg = path.node.argument;
887
- if (!arg) return;
888
-
889
- // Initialize local field maps from extracted useState fields BEFORE processing JSX
890
- const compilerState = state as CompilerState;
891
-
892
- if (compilerState.fields && compilerState.fields.length > 0) {
893
- initLocalFieldMaps(compilerState.fields);
894
- }
895
-
896
- let experienceNode: IRExperienceNode | null = null;
897
-
898
- if (t.isJSXElement(arg)) {
899
- experienceNode = jsxToExperienceNode(arg);
900
- } else if (t.isJSXFragment(arg)) {
901
- experienceNode = jsxToExperienceNode(arg);
902
- }
903
-
904
- if (experienceNode) {
905
- compilerState.experience = experienceNode;
906
- }
907
- }