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

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 (265) hide show
  1. package/ATOM-PIPELINE.md +144 -0
  2. package/README.md +88 -40
  3. package/dist/babel/index.js +2814 -277
  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-52XHYD2V.mjs +214 -0
  11. package/dist/chunk-5GUFFFGL.mjs +148 -0
  12. package/dist/chunk-5RKTOVR5.mjs +244 -0
  13. package/dist/chunk-5YDMOO4X.mjs +214 -0
  14. package/dist/chunk-64ZWEMLJ.mjs +148 -0
  15. package/dist/chunk-6XP4KSWQ.mjs +2190 -0
  16. package/dist/chunk-72QWL54I.mjs +175 -0
  17. package/dist/chunk-7B4TRI7C.mjs +4835 -0
  18. package/dist/chunk-7ZKGHTNB.mjs +4952 -0
  19. package/dist/chunk-CIESM3BP.mjs +33 -0
  20. package/dist/chunk-DE3ZGQAC.mjs +148 -0
  21. package/dist/chunk-DMCY3BBG.mjs +1933 -0
  22. package/dist/chunk-DPIK3PJS.mjs +244 -0
  23. package/dist/chunk-E5IVH4RE.mjs +186 -0
  24. package/dist/chunk-E6FZNUR5.mjs +4953 -0
  25. package/dist/chunk-EJRBDQDP.mjs +2607 -0
  26. package/dist/chunk-ELO4TXJL.mjs +186 -0
  27. package/dist/chunk-EO6SYNCG.mjs +175 -0
  28. package/dist/chunk-FKRO52XH.mjs +3446 -0
  29. package/dist/chunk-FL4YAKU6.mjs +4941 -0
  30. package/dist/chunk-FYT47UBU.mjs +5076 -0
  31. package/dist/chunk-GCLGPOJZ.mjs +148 -0
  32. package/dist/chunk-GXB4JOP7.mjs +5072 -0
  33. package/dist/chunk-HFXOUMTD.mjs +175 -0
  34. package/dist/chunk-HWIZ47US.mjs +214 -0
  35. package/dist/chunk-IB7MNPQL.mjs +4953 -0
  36. package/dist/chunk-ICSIHQCG.mjs +148 -0
  37. package/dist/chunk-J7JUAHS4.mjs +186 -0
  38. package/dist/chunk-JLA5VNQ3.mjs +186 -0
  39. package/dist/chunk-JQLWFCTM.mjs +214 -0
  40. package/dist/chunk-KFJJCQAL.mjs +148 -0
  41. package/dist/chunk-KJUIIEQE.mjs +186 -0
  42. package/dist/chunk-KNWTHRVQ.mjs +175 -0
  43. package/dist/chunk-KSG4XSZF.mjs +175 -0
  44. package/dist/chunk-LF5N6DOU.mjs +175 -0
  45. package/dist/chunk-LJQCM2IM.mjs +214 -0
  46. package/dist/chunk-NTB7OEX2.mjs +2918 -0
  47. package/dist/chunk-NW6555WJ.mjs +186 -0
  48. package/dist/chunk-OMZE6VLQ.mjs +214 -0
  49. package/dist/chunk-OPJKP747.mjs +7506 -0
  50. package/dist/chunk-P4BR7WVO.mjs +2190 -0
  51. package/dist/chunk-QQHVYH2X.mjs +244 -0
  52. package/dist/chunk-S5QLWLLT.mjs +186 -0
  53. package/dist/chunk-SCWGT2FY.mjs +2190 -0
  54. package/dist/chunk-SMKJUSB3.mjs +2190 -0
  55. package/dist/chunk-THFYE5ZX.mjs +244 -0
  56. package/dist/chunk-VCAY2KGM.mjs +175 -0
  57. package/dist/chunk-WBYMW4NQ.mjs +3450 -0
  58. package/dist/chunk-WECAV6QB.mjs +148 -0
  59. package/dist/chunk-WMKBXUCE.mjs +3228 -0
  60. package/dist/chunk-XAJ5BKKL.mjs +4947 -0
  61. package/dist/chunk-XG2X7AEA.mjs +175 -0
  62. package/dist/chunk-XG7Z23NQ.mjs +148 -0
  63. package/dist/chunk-XWZAOCQ7.mjs +2607 -0
  64. package/dist/chunk-Y6MA7ULW.mjs +148 -0
  65. package/dist/chunk-YMS7Q7LG.mjs +214 -0
  66. package/dist/chunk-ZA37XTGA.mjs +175 -0
  67. package/dist/cli/index.js +13189 -6838
  68. package/dist/cli/index.mjs +140 -22
  69. package/dist/codemod/cli.mjs +1 -1
  70. package/dist/codemod/index.mjs +1 -1
  71. package/dist/config-PL24KEWL.mjs +219 -0
  72. package/dist/dev-server-RmGHIntF.d.mts +113 -0
  73. package/dist/dev-server-RmGHIntF.d.ts +113 -0
  74. package/dist/dev-server.d.mts +1 -1
  75. package/dist/dev-server.d.ts +1 -1
  76. package/dist/dev-server.js +4135 -440
  77. package/dist/dev-server.mjs +5 -5
  78. package/dist/envelope.js +2812 -275
  79. package/dist/envelope.mjs +3 -3
  80. package/dist/index.d.mts +161 -2
  81. package/dist/index.d.ts +161 -2
  82. package/dist/index.js +4429 -428
  83. package/dist/index.mjs +217 -9
  84. package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
  85. package/dist/init-DQDX3QK6.mjs +369 -0
  86. package/dist/init-EHO4VQ22.mjs +369 -0
  87. package/dist/init-UC3FWPIW.mjs +367 -0
  88. package/dist/init-UNSMVKIK.mjs +366 -0
  89. package/dist/init-UNV5XIDE.mjs +367 -0
  90. package/dist/project-compiler-2P4N4DR7.mjs +10 -0
  91. package/dist/project-compiler-D2LCC27O.mjs +10 -0
  92. package/dist/project-compiler-EJ3GANJE.mjs +10 -0
  93. package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
  94. package/dist/project-compiler-OP2VVGJQ.mjs +10 -0
  95. package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
  96. package/dist/project-compiler-VWNNCHGO.mjs +10 -0
  97. package/dist/project-compiler-XVAAU4C5.mjs +10 -0
  98. package/dist/project-compiler-YES5FGMD.mjs +10 -0
  99. package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
  100. package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
  101. package/dist/project-decompiler-US7GAVIC.mjs +7 -0
  102. package/dist/project-decompiler-VLPR22QF.mjs +7 -0
  103. package/dist/pull-FUS5QYZS.mjs +109 -0
  104. package/dist/pull-LD5ENLGY.mjs +109 -0
  105. package/dist/pull-P44LDRWB.mjs +109 -0
  106. package/dist/testing/index.js +2822 -285
  107. package/dist/testing/index.mjs +2 -2
  108. package/dist/verify-SEIXUGN4.mjs +1833 -0
  109. package/dist/vite/index.js +2815 -278
  110. package/dist/vite/index.mjs +3 -3
  111. package/examples/uber-app/app/admin/fleet.tsx +19 -19
  112. package/package.json +16 -6
  113. package/compile-blueprint-chat.mjs +0 -99
  114. package/compile-blueprint-glass-console.mjs +0 -98
  115. package/compile-chat-defs.mjs +0 -92
  116. package/examples/uber-app/tests/payment.test.tsx +0 -129
  117. package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
  118. package/package.json.backup +0 -86
  119. package/scripts/decompile.ts +0 -226
  120. package/scripts/seed-auth.ts +0 -267
  121. package/scripts/seed-uber.ts +0 -248
  122. package/scripts/validate-uber.ts +0 -119
  123. package/seed-blueprint-chat.mjs +0 -444
  124. package/seed-blueprint-glass-console.mjs +0 -445
  125. package/seed-compiled.mjs +0 -318
  126. package/src/RoundTripValidator.ts +0 -400
  127. package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
  128. package/src/__tests__/auth-module-compilation.test.ts +0 -247
  129. package/src/__tests__/auth-template-compilation.test.ts +0 -589
  130. package/src/__tests__/change-extractor.test.ts +0 -142
  131. package/src/__tests__/cli-pull.test.ts +0 -73
  132. package/src/__tests__/cli-test.test.ts +0 -72
  133. package/src/__tests__/component-extractor.test.ts +0 -331
  134. package/src/__tests__/context-extractor.test.ts +0 -145
  135. package/src/__tests__/decompiler.test.ts +0 -718
  136. package/src/__tests__/define-blueprint.test.ts +0 -133
  137. package/src/__tests__/definition-validator.test.ts +0 -519
  138. package/src/__tests__/during-extractor.test.ts +0 -152
  139. package/src/__tests__/effect-extractor.test.ts +0 -107
  140. package/src/__tests__/event-emission.test.ts +0 -127
  141. package/src/__tests__/examples.test.ts +0 -236
  142. package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
  143. package/src/__tests__/golden-suite.test.ts +0 -403
  144. package/src/__tests__/grammar-island-extractor.test.ts +0 -289
  145. package/src/__tests__/instance-key.test.ts +0 -82
  146. package/src/__tests__/ir-migration.test.ts +0 -255
  147. package/src/__tests__/lock-file.test.ts +0 -117
  148. package/src/__tests__/model-extractor.test.ts +0 -195
  149. package/src/__tests__/model-field-acl.test.ts +0 -237
  150. package/src/__tests__/model-hooks.test.ts +0 -130
  151. package/src/__tests__/model-ref-resolution.test.ts +0 -268
  152. package/src/__tests__/model-roundtrip.test.ts +0 -502
  153. package/src/__tests__/model-runtime.test.ts +0 -112
  154. package/src/__tests__/model-transitions.test.ts +0 -183
  155. package/src/__tests__/nrt-action-trace.test.ts +0 -391
  156. package/src/__tests__/pipeline-hardening.test.ts +0 -413
  157. package/src/__tests__/project-compiler.test.ts +0 -546
  158. package/src/__tests__/project-decompiler.test.ts +0 -343
  159. package/src/__tests__/query-compilation.test.ts +0 -145
  160. package/src/__tests__/round-trip/PLAN.md +0 -158
  161. package/src/__tests__/round-trip/README.md +0 -52
  162. package/src/__tests__/round-trip/RESULTS.md +0 -86
  163. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
  164. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
  165. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
  166. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
  167. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
  168. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
  169. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
  170. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
  171. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
  172. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
  173. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
  174. package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
  175. package/src/__tests__/round-trip-ir.test.ts +0 -300
  176. package/src/__tests__/round-trip.test.ts +0 -1212
  177. package/src/__tests__/route-merging.test.ts +0 -372
  178. package/src/__tests__/router-composition.test.ts +0 -489
  179. package/src/__tests__/router-extractor.test.ts +0 -176
  180. package/src/__tests__/server-action-extractor.test.ts +0 -128
  181. package/src/__tests__/smart-type-inference.test.ts +0 -365
  182. package/src/__tests__/source-envelope.test.ts +0 -284
  183. package/src/__tests__/source-fidelity.test.ts +0 -516
  184. package/src/__tests__/state-extractor.test.ts +0 -115
  185. package/src/__tests__/strict-mode.test.ts +0 -227
  186. package/src/__tests__/transition-effect-extractor.test.ts +0 -119
  187. package/src/__tests__/transition-extractor.test.ts +0 -68
  188. package/src/__tests__/ts-to-expression.test.ts +0 -462
  189. package/src/__tests__/type-generator.test.ts +0 -201
  190. package/src/__tests__/uber-validation.test.ts +0 -502
  191. package/src/action-compiler.ts +0 -361
  192. package/src/babel/emitters/experience-transform.ts +0 -199
  193. package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
  194. package/src/babel/emitters/pure-form-emitter.ts +0 -1023
  195. package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
  196. package/src/babel/extractors/change-extractor.ts +0 -199
  197. package/src/babel/extractors/component-extractor.ts +0 -907
  198. package/src/babel/extractors/computed-extractor.ts +0 -262
  199. package/src/babel/extractors/context-extractor.ts +0 -277
  200. package/src/babel/extractors/during-extractor.ts +0 -295
  201. package/src/babel/extractors/effect-extractor.ts +0 -340
  202. package/src/babel/extractors/event-extractor.ts +0 -235
  203. package/src/babel/extractors/grammar-island-extractor.ts +0 -302
  204. package/src/babel/extractors/model-extractor.ts +0 -1018
  205. package/src/babel/extractors/router-extractor.ts +0 -303
  206. package/src/babel/extractors/server-action-extractor.ts +0 -173
  207. package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
  208. package/src/babel/extractors/server-state-extractor.ts +0 -88
  209. package/src/babel/extractors/state-extractor.ts +0 -214
  210. package/src/babel/extractors/transition-effect-extractor.ts +0 -176
  211. package/src/babel/extractors/transition-extractor.ts +0 -143
  212. package/src/babel/index.ts +0 -24
  213. package/src/babel/transpilers/ts-to-expression.ts +0 -674
  214. package/src/babel/visitor.ts +0 -807
  215. package/src/cli/auth.ts +0 -255
  216. package/src/cli/build.ts +0 -288
  217. package/src/cli/deploy.ts +0 -206
  218. package/src/cli/index.ts +0 -328
  219. package/src/cli/installer.ts +0 -261
  220. package/src/cli/lock-file.ts +0 -94
  221. package/src/cli/mmrc.ts +0 -22
  222. package/src/cli/pull.ts +0 -172
  223. package/src/cli/registry-client.ts +0 -175
  224. package/src/cli/test.ts +0 -397
  225. package/src/cli/type-generator.ts +0 -243
  226. package/src/codemod/__tests__/forward.test.ts +0 -239
  227. package/src/codemod/__tests__/reverse.test.ts +0 -145
  228. package/src/codemod/__tests__/round-trip.test.ts +0 -137
  229. package/src/codemod/annotation.ts +0 -97
  230. package/src/codemod/classify.ts +0 -197
  231. package/src/codemod/cli.ts +0 -207
  232. package/src/codemod/control-flow.ts +0 -409
  233. package/src/codemod/forward.ts +0 -244
  234. package/src/codemod/import-manager.ts +0 -171
  235. package/src/codemod/index.ts +0 -120
  236. package/src/codemod/reverse.ts +0 -197
  237. package/src/codemod/rules.ts +0 -174
  238. package/src/codemod/state-transform.ts +0 -126
  239. package/src/decompiler/ast-builder.ts +0 -538
  240. package/src/decompiler/config-generator.ts +0 -151
  241. package/src/decompiler/index.ts +0 -315
  242. package/src/decompiler/project-decompiler.ts +0 -1776
  243. package/src/decompiler/project.ts +0 -862
  244. package/src/decompiler/split-strategy.ts +0 -140
  245. package/src/decompiler/state-emitter.ts +0 -1053
  246. package/src/decompiler/sx-emitter.ts +0 -318
  247. package/src/decompiler/workspace-hydrator.ts +0 -189
  248. package/src/dev-server.ts +0 -238
  249. package/src/envelope/fs-tree.ts +0 -217
  250. package/src/envelope/source-envelope.ts +0 -264
  251. package/src/envelope.ts +0 -315
  252. package/src/incremental-compiler.ts +0 -401
  253. package/src/index.ts +0 -99
  254. package/src/model-compiler.ts +0 -277
  255. package/src/project-compiler.ts +0 -1629
  256. package/src/route-extractor.ts +0 -333
  257. package/src/testing/index.ts +0 -32
  258. package/src/testing/snapshot.ts +0 -252
  259. package/src/testing/test-utils.ts +0 -226
  260. package/src/types.ts +0 -68
  261. package/src/vite/index.ts +0 -288
  262. package/test-compile.mjs +0 -142
  263. package/tsconfig.json +0 -25
  264. package/tsup.config.ts +0 -23
  265. package/vitest.config.ts +0 -9
@@ -1,137 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { transform } from '../index';
3
-
4
- /**
5
- * Round-trip invariant: forward(source) → reverse → ≈ source
6
- *
7
- * "≈" means semantically equivalent — whitespace may differ,
8
- * and plain divs may gain flex/flex-col (since atoms default to Stack).
9
- */
10
-
11
- function roundTrip(source: string): string {
12
- const forwarded = transform(source, { direction: 'forward', annotate: true });
13
- const reversed = transform(forwarded.code, { direction: 'reverse', annotate: false });
14
- return reversed.code;
15
- }
16
-
17
- function normalize(s: string): string {
18
- return s.replace(/\s+/g, ' ').trim();
19
- }
20
-
21
- describe('round-trip invariant', () => {
22
- it('preserves a simple flex row', () => {
23
- const input = `const App = () => <div className="flex items-center gap-2">hello</div>;`;
24
- const result = roundTrip(input);
25
- expect(normalize(result)).toContain('flex items-center gap-2');
26
- expect(result).toContain('<div');
27
- });
28
-
29
- it('preserves a flex-col stack', () => {
30
- const input = `const App = () => <div className="flex flex-col gap-4 p-2">hello</div>;`;
31
- const result = roundTrip(input);
32
- expect(normalize(result)).toContain('flex flex-col gap-4 p-2');
33
- });
34
-
35
- it('preserves conditional rendering', () => {
36
- const input = `const App = () => <div className="flex">{visible && <span className="text-sm">hi</span>}</div>;`;
37
- const result = roundTrip(input);
38
- expect(result).toContain('visible &&');
39
- expect(result).toContain('<span');
40
- });
41
-
42
- it('preserves .map() rendering', () => {
43
- const input = `const App = () => (
44
- <div className="flex flex-col">
45
- {items.map(item => <span key={item.id}>{item.name}</span>)}
46
- </div>
47
- );`;
48
- const result = roundTrip(input);
49
- expect(result).toContain('.map(');
50
- expect(result).toContain('<span');
51
- });
52
-
53
- it('preserves button with onClick', () => {
54
- const input = `const App = () => <button className="px-4 py-2" onClick={handleClick}>Go</button>;`;
55
- const result = roundTrip(input);
56
- expect(result).toContain('<button');
57
- expect(result).toContain('onClick');
58
- expect(result).toContain('px-4 py-2');
59
- });
60
-
61
- it('preserves img', () => {
62
- const input = `const App = () => <img src="/avatar.png" className="w-9 h-9 rounded-full" />;`;
63
- const result = roundTrip(input);
64
- expect(result).toContain('<img');
65
- expect(result).toContain('w-9 h-9 rounded-full');
66
- expect(result).toContain('src="/avatar.png"');
67
- });
68
-
69
- it('preserves headings', () => {
70
- const input = `const App = () => <h2 className="font-bold text-lg">Title</h2>;`;
71
- const result = roundTrip(input);
72
- expect(result).toContain('<h2');
73
- expect(result).toContain('font-bold');
74
- });
75
-
76
- it('preserves textarea', () => {
77
- const input = `const App = () => <textarea className="w-full h-20" />;`;
78
- const result = roundTrip(input);
79
- expect(result).toContain('<textarea');
80
- expect(result).toContain('w-full h-20');
81
- });
82
-
83
- it('preserves a full component with mixed patterns', () => {
84
- const input = `
85
- import React, { useState } from 'react';
86
- import { Hash } from 'lucide-react';
87
-
88
- const ChannelItem = ({ channel, isActive, onSelect }) => {
89
- const [hovering, setHovering] = useState(false);
90
-
91
- return (
92
- <div
93
- className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-800 cursor-pointer"
94
- onClick={() => onSelect(channel.id)}
95
- onMouseEnter={() => setHovering(true)}
96
- onMouseLeave={() => setHovering(false)}
97
- >
98
- <Hash className="w-3.5 h-3.5 text-gray-400 shrink-0" />
99
- <span className="text-sm text-gray-300 truncate flex-1">
100
- {channel.name}
101
- </span>
102
- {channel.unreadCount > 0 && (
103
- <span className="bg-blue-500 text-white text-xs px-1.5 rounded-full min-w-[18px] text-center">
104
- {channel.unreadCount}
105
- </span>
106
- )}
107
- </div>
108
- );
109
- };`;
110
- const result = roundTrip(input);
111
-
112
- // All key features preserved
113
- expect(result).toContain('useState');
114
- expect(result).toContain('Hash');
115
- expect(result).toContain('channel.name');
116
- expect(result).toContain('channel.unreadCount');
117
- expect(result).toContain('hover:bg-gray-800');
118
- expect(result).toContain('cursor-pointer');
119
- expect(result).toContain('lucide-react');
120
- expect(result).toContain('onMouseEnter');
121
- expect(result).toContain('onMouseLeave');
122
- });
123
-
124
- it('preserves motion.div (not transformed)', () => {
125
- const input = `const App = () => <motion.div className="flex" initial={{ opacity: 0 }}>hi</motion.div>;`;
126
- const result = roundTrip(input);
127
- expect(result).toContain('motion.div');
128
- expect(result).toContain('initial=');
129
- });
130
-
131
- it('preserves PascalCase components (not transformed)', () => {
132
- const input = `const App = () => <AnimatePresence><MyWidget>hi</MyWidget></AnimatePresence>;`;
133
- const result = roundTrip(input);
134
- expect(result).toContain('<AnimatePresence');
135
- expect(result).toContain('<MyWidget');
136
- });
137
- });
@@ -1,97 +0,0 @@
1
- /**
2
- * Round-trip annotation — preserves original element info as JSDoc comments.
3
- *
4
- * Format: @mm-original tagName.class1.class2.class3
5
- */
6
-
7
- import * as t from '@babel/types';
8
-
9
- const ANNOTATION_PREFIX = '@mm-original';
10
-
11
- /**
12
- * Create an annotation comment for a transformed element.
13
- */
14
- export function createAnnotation(
15
- originalTag: string,
16
- classNames: string[],
17
- ): string {
18
- const parts = [originalTag, ...classNames.slice(0, 10)]; // limit to 10 classes
19
- return `${ANNOTATION_PREFIX} ${parts.join('.')}`;
20
- }
21
-
22
- /**
23
- * Parse an annotation comment.
24
- * Returns { tag, classes } or null if not an annotation.
25
- */
26
- export function parseAnnotation(
27
- comment: string,
28
- ): { tag: string; classes: string[] } | null {
29
- const trimmed = comment.trim();
30
- if (!trimmed.startsWith(ANNOTATION_PREFIX)) return null;
31
-
32
- const rest = trimmed.slice(ANNOTATION_PREFIX.length).trim();
33
- if (!rest) return null;
34
-
35
- const parts = rest.split('.');
36
- const tag = parts[0];
37
- const classes = parts.slice(1);
38
-
39
- return { tag, classes };
40
- }
41
-
42
- /**
43
- * Add a leading comment annotation to a JSX element node.
44
- */
45
- export function addAnnotationComment(
46
- node: t.Node,
47
- originalTag: string,
48
- classNames: string[],
49
- ): void {
50
- const text = ` ${createAnnotation(originalTag, classNames)} `;
51
- if (!node.leadingComments) {
52
- node.leadingComments = [];
53
- }
54
- node.leadingComments.push({
55
- type: 'CommentBlock',
56
- value: text,
57
- } as t.Comment);
58
- }
59
-
60
- /**
61
- * Read and remove the annotation comment from a node.
62
- * Returns parsed annotation or null.
63
- */
64
- export function readAnnotationComment(
65
- node: t.Node,
66
- ): { tag: string; classes: string[] } | null {
67
- if (!node.leadingComments) return null;
68
-
69
- for (let i = node.leadingComments.length - 1; i >= 0; i--) {
70
- const comment = node.leadingComments[i];
71
- const parsed = parseAnnotation(comment.value);
72
- if (parsed) {
73
- // Remove the annotation comment
74
- node.leadingComments.splice(i, 1);
75
- return parsed;
76
- }
77
- }
78
-
79
- return null;
80
- }
81
-
82
- /**
83
- * Strip all @mm-original annotations from a node and its children.
84
- * Used with --strip-annotations flag.
85
- */
86
- export function stripAnnotations(node: t.Node): void {
87
- if (node.leadingComments) {
88
- node.leadingComments = node.leadingComments.filter(
89
- c => !parseAnnotation(c.value),
90
- );
91
- }
92
- if (node.trailingComments) {
93
- node.trailingComments = node.trailingComments.filter(
94
- c => !parseAnnotation(c.value),
95
- );
96
- }
97
- }
@@ -1,197 +0,0 @@
1
- /**
2
- * Element classifier — analyzes tag name + className to determine target atom.
3
- */
4
-
5
- import type * as t from '@babel/types';
6
- import { FORWARD_RULES, REVERSE_RULES, CONTROL_FLOW_ATOMS, TEXT_VARIANT_TO_TAG } from './rules';
7
- import type { MappingRule } from './rules';
8
-
9
- export interface ClassifyResult {
10
- atom: string;
11
- /** Classes to remove from className (implicit in atom) */
12
- removeClasses: string[];
13
- /** Props to add to the element */
14
- addProps: Record<string, string | boolean>;
15
- /** Original tag for annotation */
16
- originalTag: string;
17
- }
18
-
19
- export interface ReverseClassifyResult {
20
- htmlTag: string;
21
- /** Classes to inject into className */
22
- injectClasses: string[];
23
- /** Props to remove (encoded in HTML tag) */
24
- removeProps: string[];
25
- }
26
-
27
- /**
28
- * Extract static class names from a className JSX attribute value.
29
- * Handles: string literals, template literals (static parts), cn() first arg.
30
- */
31
- export function extractStaticClasses(attrValue: t.Node | null | undefined): string[] {
32
- if (!attrValue) return [];
33
-
34
- // className="flex items-center"
35
- if (attrValue.type === 'StringLiteral') {
36
- return attrValue.value.split(/\s+/).filter(Boolean);
37
- }
38
-
39
- // className={expr} — unwrap JSXExpressionContainer
40
- if (attrValue.type === 'JSXExpressionContainer') {
41
- return extractStaticClasses(attrValue.expression as t.Node);
42
- }
43
-
44
- // className={`flex items-center ${dynamic}`}
45
- if (attrValue.type === 'TemplateLiteral') {
46
- const staticParts = attrValue.quasis.map(q => q.value.raw).join(' ');
47
- return staticParts.split(/\s+/).filter(Boolean);
48
- }
49
-
50
- // className={cn("flex items-center", dynamic)}
51
- if (
52
- attrValue.type === 'CallExpression' &&
53
- attrValue.callee.type === 'Identifier' &&
54
- attrValue.callee.name === 'cn' &&
55
- attrValue.arguments.length > 0
56
- ) {
57
- const firstArg = attrValue.arguments[0];
58
- if (firstArg.type === 'StringLiteral') {
59
- return firstArg.value.split(/\s+/).filter(Boolean);
60
- }
61
- }
62
-
63
- return [];
64
- }
65
-
66
- /**
67
- * Classify a raw HTML element into its target workflow atom.
68
- */
69
- export function classifyElement(
70
- tagName: string,
71
- classNames: string[],
72
- ): ClassifyResult | null {
73
- // Skip elements that are already atoms or components (PascalCase)
74
- if (tagName[0] === tagName[0].toUpperCase() && tagName[0] !== tagName[0].toLowerCase()) {
75
- return null;
76
- }
77
-
78
- // Skip motion.* elements
79
- if (tagName.startsWith('motion.')) {
80
- return null;
81
- }
82
-
83
- // Find matching rules for this tag, sorted by priority descending
84
- const candidates = FORWARD_RULES
85
- .filter(r => r.htmlTag === tagName)
86
- .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
87
-
88
- if (candidates.length === 0) return null;
89
-
90
- // For div, we need to check className signals
91
- if (tagName === 'div') {
92
- for (const rule of candidates) {
93
- if (!rule.classSignal || rule.classSignal.length === 0) {
94
- // Default rule (no signal required) — only use if no better match
95
- continue;
96
- }
97
- const hasAllSignals = rule.classSignal.every(s => classNames.includes(s));
98
- if (hasAllSignals) {
99
- return makeResult(rule, tagName);
100
- }
101
- }
102
- // Fallback: default div rule (priority 0)
103
- const defaultRule = candidates.find(r => (r.priority ?? 0) === 0);
104
- if (defaultRule) {
105
- return makeResult(defaultRule, tagName);
106
- }
107
- }
108
-
109
- // For non-div elements, take the first (and usually only) match
110
- const rule = candidates[0];
111
- return makeResult(rule, tagName);
112
- }
113
-
114
- function makeResult(rule: MappingRule, tagName: string): ClassifyResult {
115
- return {
116
- atom: rule.atom,
117
- removeClasses: rule.implicitClasses ?? [],
118
- addProps: rule.props ?? {},
119
- originalTag: tagName,
120
- };
121
- }
122
-
123
- /**
124
- * Reverse-classify a workflow atom into its target HTML element.
125
- */
126
- export function reverseClassifyAtom(
127
- atomName: string,
128
- props: Map<string, t.Node>,
129
- ): ReverseClassifyResult | null {
130
- // Skip control flow atoms
131
- if (CONTROL_FLOW_ATOMS.has(atomName)) return null;
132
-
133
- // Special case: Text with variant → heading tag
134
- if (atomName === 'Text') {
135
- const variantNode = props.get('variant');
136
- if (variantNode && variantNode.type === 'StringLiteral') {
137
- const tag = TEXT_VARIANT_TO_TAG[variantNode.value];
138
- if (tag) {
139
- return {
140
- htmlTag: tag,
141
- injectClasses: [],
142
- removeProps: ['variant'],
143
- };
144
- }
145
- }
146
- // Default Text → span
147
- return { htmlTag: 'span', injectClasses: [], removeProps: [] };
148
- }
149
-
150
- // Special case: TextInput with multiline → textarea
151
- if (atomName === 'TextInput') {
152
- const multilineNode = props.get('multiline');
153
- if (multilineNode) {
154
- const isMultiline =
155
- (multilineNode.type === 'BooleanLiteral' && multilineNode.value) ||
156
- (multilineNode.type === 'JSXExpressionContainer' &&
157
- (multilineNode as any).expression?.type === 'BooleanLiteral' &&
158
- (multilineNode as any).expression?.value);
159
- if (isMultiline) {
160
- return { htmlTag: 'textarea', injectClasses: [], removeProps: ['multiline'] };
161
- }
162
- }
163
- return { htmlTag: 'input', injectClasses: [], removeProps: [] };
164
- }
165
-
166
- const rule = REVERSE_RULES.find(r => r.atom === atomName);
167
- if (!rule) return null;
168
-
169
- return {
170
- htmlTag: rule.htmlTag,
171
- injectClasses: rule.injectClasses ?? [],
172
- removeProps: rule.removeProps ?? [],
173
- };
174
- }
175
-
176
- /**
177
- * Remove classes from a className string.
178
- */
179
- export function removeClassesFromString(className: string, toRemove: string[]): string {
180
- if (toRemove.length === 0) return className;
181
- const removeSet = new Set(toRemove);
182
- return className
183
- .split(/\s+/)
184
- .filter(c => c && !removeSet.has(c))
185
- .join(' ');
186
- }
187
-
188
- /**
189
- * Add classes to the beginning of a className string.
190
- */
191
- export function addClassesToString(className: string, toAdd: string[]): string {
192
- if (toAdd.length === 0) return className;
193
- const existing = new Set(className.split(/\s+/).filter(Boolean));
194
- const newClasses = toAdd.filter(c => !existing.has(c));
195
- if (newClasses.length === 0) return className;
196
- return [...newClasses, ...className.split(/\s+/).filter(Boolean)].join(' ');
197
- }
@@ -1,207 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CLI for the bidirectional React ↔ Workflow Atom codemod.
4
- *
5
- * Usage:
6
- * npx mm-codemod forward src/features/chat/
7
- * npx mm-codemod reverse packages/blueprint-chat/app/
8
- * npx mm-codemod verify src/features/chat/
9
- */
10
-
11
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
12
- import { resolve, relative, dirname } from 'path';
13
- import { glob } from 'glob';
14
- import { transform, type CodemodOptions } from './index';
15
- import type { StateMode } from './state-transform';
16
-
17
- interface CLIOptions {
18
- direction: 'forward' | 'reverse' | 'verify';
19
- paths: string[];
20
- dryRun: boolean;
21
- stripAnnotations: boolean;
22
- stateMode: StateMode;
23
- verbose: boolean;
24
- outDir?: string;
25
- }
26
-
27
- function parseArgs(argv: string[]): CLIOptions {
28
- const args = argv.slice(2);
29
- const direction = args[0] as CLIOptions['direction'];
30
-
31
- if (!direction || !['forward', 'reverse', 'verify'].includes(direction)) {
32
- console.error('Usage: mm-codemod <forward|reverse|verify> <paths...> [options]');
33
- console.error('');
34
- console.error('Options:');
35
- console.error(' --dry-run Print diff without writing');
36
- console.error(' --strip-annotations Remove @mm-original comments');
37
- console.error(' --mode <mode> State mode: preserve|migrate|annotate');
38
- console.error(' --out <dir> Output directory (default: in-place)');
39
- console.error(' --verbose Log every transformation');
40
- process.exit(1);
41
- }
42
-
43
- const paths: string[] = [];
44
- let dryRun = false;
45
- let stripAnnotations = false;
46
- let stateMode: StateMode = 'preserve';
47
- let verbose = false;
48
- let outDir: string | undefined;
49
-
50
- for (let i = 1; i < args.length; i++) {
51
- const arg = args[i];
52
- if (arg === '--dry-run') {
53
- dryRun = true;
54
- } else if (arg === '--strip-annotations') {
55
- stripAnnotations = true;
56
- } else if (arg === '--mode') {
57
- stateMode = args[++i] as StateMode;
58
- } else if (arg === '--out') {
59
- outDir = args[++i];
60
- } else if (arg === '--verbose') {
61
- verbose = true;
62
- } else if (!arg.startsWith('-')) {
63
- paths.push(arg);
64
- }
65
- }
66
-
67
- if (paths.length === 0) {
68
- console.error('Error: No paths specified');
69
- process.exit(1);
70
- }
71
-
72
- return { direction, paths, dryRun, stripAnnotations, stateMode, verbose, outDir };
73
- }
74
-
75
- async function findFiles(paths: string[]): Promise<string[]> {
76
- const files: string[] = [];
77
-
78
- for (const p of paths) {
79
- const resolved = resolve(p);
80
- const matches = await glob('**/*.{tsx,jsx}', {
81
- cwd: resolved,
82
- absolute: true,
83
- ignore: ['**/node_modules/**', '**/__tests__/**', '**/dist/**'],
84
- });
85
- files.push(...matches);
86
- }
87
-
88
- return files.sort();
89
- }
90
-
91
- function transformFile(
92
- filePath: string,
93
- options: CodemodOptions,
94
- verbose: boolean,
95
- ): { code: string; changed: boolean } {
96
- const source = readFileSync(filePath, 'utf-8');
97
-
98
- try {
99
- const result = transform(source, options);
100
- const changed = result.code !== source;
101
-
102
- if (verbose && changed) {
103
- console.log(` [${options.direction}] ${relative(process.cwd(), filePath)}`);
104
- }
105
-
106
- return { code: result.code, changed };
107
- } catch (err: any) {
108
- console.error(` [ERROR] ${relative(process.cwd(), filePath)}: ${err.message}`);
109
- return { code: source, changed: false };
110
- }
111
- }
112
-
113
- async function main() {
114
- const opts = parseArgs(process.argv);
115
- const files = await findFiles(opts.paths);
116
-
117
- console.log(`mm-codemod: ${opts.direction} — ${files.length} file(s)`);
118
-
119
- if (opts.direction === 'verify') {
120
- // Round-trip verification: forward(source) → reverse(result) → diff
121
- let pass = 0;
122
- let fail = 0;
123
-
124
- for (const file of files) {
125
- const source = readFileSync(file, 'utf-8');
126
- const forward = transform(source, {
127
- direction: 'forward',
128
- annotate: true,
129
- stateMode: opts.stateMode,
130
- });
131
- const roundTripped = transform(forward.code, {
132
- direction: 'reverse',
133
- annotate: false,
134
- stateMode: opts.stateMode,
135
- });
136
-
137
- // Normalize whitespace for comparison
138
- const normalize = (s: string) => s.replace(/\s+/g, ' ').trim();
139
- if (normalize(roundTripped.code) === normalize(source)) {
140
- pass++;
141
- if (opts.verbose) console.log(` [PASS] ${relative(process.cwd(), file)}`);
142
- } else {
143
- fail++;
144
- console.log(` [FAIL] ${relative(process.cwd(), file)}`);
145
- if (opts.verbose) {
146
- console.log(' Expected (normalized):');
147
- console.log(` ${normalize(source).slice(0, 200)}...`);
148
- console.log(' Got (normalized):');
149
- console.log(` ${normalize(roundTripped.code).slice(0, 200)}...`);
150
- }
151
- }
152
- }
153
-
154
- console.log(`\nResults: ${pass} pass, ${fail} fail out of ${files.length} files`);
155
- process.exit(fail > 0 ? 1 : 0);
156
- }
157
-
158
- // Forward or reverse
159
- const codemodOptions: CodemodOptions = {
160
- direction: opts.direction as 'forward' | 'reverse',
161
- annotate: !opts.stripAnnotations,
162
- stateMode: opts.stateMode,
163
- };
164
-
165
- let changed = 0;
166
- let unchanged = 0;
167
- let errors = 0;
168
-
169
- for (const file of files) {
170
- try {
171
- const result = transformFile(file, codemodOptions, opts.verbose);
172
-
173
- if (!result.changed) {
174
- unchanged++;
175
- continue;
176
- }
177
-
178
- changed++;
179
-
180
- if (opts.dryRun) {
181
- console.log(` [WOULD CHANGE] ${relative(process.cwd(), file)}`);
182
- continue;
183
- }
184
-
185
- // Determine output path
186
- let outPath = file;
187
- if (opts.outDir) {
188
- const rel = relative(resolve(opts.paths[0]), file);
189
- outPath = resolve(opts.outDir, rel);
190
- const dir = dirname(outPath);
191
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
192
- }
193
-
194
- writeFileSync(outPath, result.code, 'utf-8');
195
- } catch (err: any) {
196
- errors++;
197
- console.error(` [ERROR] ${relative(process.cwd(), file)}: ${err.message}`);
198
- }
199
- }
200
-
201
- console.log(`\nDone: ${changed} changed, ${unchanged} unchanged, ${errors} errors`);
202
- }
203
-
204
- main().catch(err => {
205
- console.error(err);
206
- process.exit(1);
207
- });