@mmapp/react-compiler 0.1.0-alpha.1

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 (314) hide show
  1. package/README.md +107 -0
  2. package/compile-blueprint-chat.mjs +99 -0
  3. package/compile-blueprint-glass-console.mjs +98 -0
  4. package/compile-chat-defs.mjs +92 -0
  5. package/dist/babel/index.d.mts +3 -0
  6. package/dist/babel/index.d.ts +3 -0
  7. package/dist/babel/index.js +4851 -0
  8. package/dist/babel/index.mjs +7 -0
  9. package/dist/chunk-26U577GB.mjs +3465 -0
  10. package/dist/chunk-2FBDFAX6.mjs +2362 -0
  11. package/dist/chunk-2L4QSMXG.mjs +175 -0
  12. package/dist/chunk-2REDFOER.mjs +931 -0
  13. package/dist/chunk-46YKSHQR.mjs +175 -0
  14. package/dist/chunk-4XHK6FWL.mjs +2058 -0
  15. package/dist/chunk-5M7DKKBC.mjs +215 -0
  16. package/dist/chunk-5VNJ7C6N.mjs +154 -0
  17. package/dist/chunk-6CQOAAMV.mjs +1803 -0
  18. package/dist/chunk-6SEVAAVT.mjs +3516 -0
  19. package/dist/chunk-6YLR5ZDA.mjs +2829 -0
  20. package/dist/chunk-AOGY2GK6.mjs +3292 -0
  21. package/dist/chunk-AXXUXRNA.mjs +1434 -0
  22. package/dist/chunk-CHLVKMQW.mjs +175 -0
  23. package/dist/chunk-CKGOZAB7.mjs +939 -0
  24. package/dist/chunk-D34RAZUX.mjs +2223 -0
  25. package/dist/chunk-EQGA6A6D.mjs +121 -0
  26. package/dist/chunk-EY2CSXYA.mjs +822 -0
  27. package/dist/chunk-FIQ65CDR.mjs +925 -0
  28. package/dist/chunk-FOZXJFAR.mjs +186 -0
  29. package/dist/chunk-FX6URXWN.mjs +186 -0
  30. package/dist/chunk-G7SMOWOL.mjs +828 -0
  31. package/dist/chunk-GGB4G5YY.mjs +175 -0
  32. package/dist/chunk-HLRGCCIL.mjs +4839 -0
  33. package/dist/chunk-HOIUP6IF.mjs +690 -0
  34. package/dist/chunk-I3AU7GRD.mjs +120 -0
  35. package/dist/chunk-ILFGMUVD.mjs +1933 -0
  36. package/dist/chunk-IPTX5MJU.mjs +3223 -0
  37. package/dist/chunk-ITGUSH2Z.mjs +2783 -0
  38. package/dist/chunk-IXHBCAMF.mjs +3306 -0
  39. package/dist/chunk-J7TWJ3TM.mjs +2784 -0
  40. package/dist/chunk-JDPLDGVF.mjs +4810 -0
  41. package/dist/chunk-K53XP2DL.mjs +148 -0
  42. package/dist/chunk-K5HX2SVL.mjs +1902 -0
  43. package/dist/chunk-KFGYOOVS.mjs +214 -0
  44. package/dist/chunk-KFVVOS5N.mjs +925 -0
  45. package/dist/chunk-L2OZ4CDV.mjs +113 -0
  46. package/dist/chunk-MIZV3TAN.mjs +3293 -0
  47. package/dist/chunk-NKKLQE5V.mjs +148 -0
  48. package/dist/chunk-NOW23XFZ.mjs +186 -0
  49. package/dist/chunk-NRXQKQ74.mjs +148 -0
  50. package/dist/chunk-OWI6XWCD.mjs +3375 -0
  51. package/dist/chunk-PRUMNNDI.mjs +3192 -0
  52. package/dist/chunk-QTBD5B3F.mjs +148 -0
  53. package/dist/chunk-SKSDPPNT.mjs +3788 -0
  54. package/dist/chunk-SP2YUS33.mjs +186 -0
  55. package/dist/chunk-SU4E6E7B.mjs +3153 -0
  56. package/dist/chunk-SYUUKW5A.mjs +3379 -0
  57. package/dist/chunk-UL2XZEMA.mjs +3128 -0
  58. package/dist/chunk-XMWUHQVV.mjs +939 -0
  59. package/dist/chunk-XZNEDRGN.mjs +3876 -0
  60. package/dist/chunk-Y6FXYEAI.mjs +10 -0
  61. package/dist/chunk-YFS6JMYO.mjs +3342 -0
  62. package/dist/chunk-Z6AIQ4KL.mjs +113 -0
  63. package/dist/cli/index.d.mts +1 -0
  64. package/dist/cli/index.d.ts +1 -0
  65. package/dist/cli/index.js +11585 -0
  66. package/dist/cli/index.mjs +701 -0
  67. package/dist/codemod/cli.d.mts +1 -0
  68. package/dist/codemod/cli.d.ts +1 -0
  69. package/dist/codemod/cli.js +1104 -0
  70. package/dist/codemod/cli.mjs +157 -0
  71. package/dist/codemod/index.d.mts +148 -0
  72. package/dist/codemod/index.d.ts +148 -0
  73. package/dist/codemod/index.js +981 -0
  74. package/dist/codemod/index.mjs +25 -0
  75. package/dist/dev-server-Bs_sz2DG.d.mts +111 -0
  76. package/dist/dev-server-Bs_sz2DG.d.ts +111 -0
  77. package/dist/dev-server-CjoufJ-u.d.mts +109 -0
  78. package/dist/dev-server-CjoufJ-u.d.ts +109 -0
  79. package/dist/dev-server.d.mts +3 -0
  80. package/dist/dev-server.d.ts +3 -0
  81. package/dist/dev-server.js +7603 -0
  82. package/dist/dev-server.mjs +11 -0
  83. package/dist/envelope-DD7v0v6E.d.mts +265 -0
  84. package/dist/envelope-DD7v0v6E.d.ts +265 -0
  85. package/dist/envelope-vCVjrHlo.d.mts +265 -0
  86. package/dist/envelope-vCVjrHlo.d.ts +265 -0
  87. package/dist/envelope.d.mts +2 -0
  88. package/dist/envelope.d.ts +2 -0
  89. package/dist/envelope.js +5184 -0
  90. package/dist/envelope.mjs +9 -0
  91. package/dist/index-B5gSgvnd.d.mts +44 -0
  92. package/dist/index-B5gSgvnd.d.ts +44 -0
  93. package/dist/index-Bs0MnR54.d.mts +103 -0
  94. package/dist/index-Bs0MnR54.d.ts +103 -0
  95. package/dist/index-DR0nNc_f.d.mts +101 -0
  96. package/dist/index-DR0nNc_f.d.ts +101 -0
  97. package/dist/index-revho_gS.d.mts +104 -0
  98. package/dist/index-revho_gS.d.ts +104 -0
  99. package/dist/index.d.mts +1099 -0
  100. package/dist/index.d.ts +1099 -0
  101. package/dist/index.js +10162 -0
  102. package/dist/index.mjs +372 -0
  103. package/dist/init-IXEE2RCF.mjs +340 -0
  104. package/dist/project-compiler-EGJUTAJU.mjs +10 -0
  105. package/dist/project-compiler-VFR6CSDX.mjs +10 -0
  106. package/dist/project-decompiler-5GY2KSG4.mjs +7 -0
  107. package/dist/pull-A2QUHW4K.mjs +109 -0
  108. package/dist/pull-JBEQWVPE.mjs +109 -0
  109. package/dist/testing/index.d.mts +211 -0
  110. package/dist/testing/index.d.ts +211 -0
  111. package/dist/testing/index.js +5106 -0
  112. package/dist/testing/index.mjs +247 -0
  113. package/dist/vite/index.d.mts +59 -0
  114. package/dist/vite/index.d.ts +59 -0
  115. package/dist/vite/index.js +5023 -0
  116. package/dist/vite/index.mjs +8 -0
  117. package/examples/README.md +72 -0
  118. package/examples/authentication/main.workflow.tsx +139 -0
  119. package/examples/authentication/mm.config.ts +22 -0
  120. package/examples/authentication/models/auth.ts +45 -0
  121. package/examples/authentication/pages/LoginPage.tsx +79 -0
  122. package/examples/authentication/pages/SignupPage.tsx +87 -0
  123. package/examples/counter.workflow.tsx +65 -0
  124. package/examples/dashboard.workflow.tsx +419 -0
  125. package/examples/invoice-approval/actions/invoice.server.ts +72 -0
  126. package/examples/invoice-approval/main.workflow.tsx +168 -0
  127. package/examples/invoice-approval/mm.config.ts +18 -0
  128. package/examples/invoice-approval/models/invoice.ts +46 -0
  129. package/examples/invoice-approval/pages/InvoiceDetailPage.tsx +175 -0
  130. package/examples/invoice-approval/pages/InvoiceFormPage.tsx +198 -0
  131. package/examples/invoice-approval/pages/InvoiceListPage.tsx +141 -0
  132. package/examples/todo-app.workflow.tsx +131 -0
  133. package/examples/uber-app/actions/matching.server.ts +177 -0
  134. package/examples/uber-app/actions/notifications.server.ts +176 -0
  135. package/examples/uber-app/actions/payments.server.ts +184 -0
  136. package/examples/uber-app/actions/pricing.server.ts +176 -0
  137. package/examples/uber-app/app/admin/analytics.tsx +102 -0
  138. package/examples/uber-app/app/admin/fleet.tsx +102 -0
  139. package/examples/uber-app/app/admin/surge-pricing.tsx +95 -0
  140. package/examples/uber-app/app/driver/dashboard.tsx +87 -0
  141. package/examples/uber-app/app/driver/earnings.tsx +101 -0
  142. package/examples/uber-app/app/driver/navigation.tsx +94 -0
  143. package/examples/uber-app/app/driver/ride-acceptance.tsx +103 -0
  144. package/examples/uber-app/app/rider/home.tsx +109 -0
  145. package/examples/uber-app/app/rider/payment-methods.tsx +134 -0
  146. package/examples/uber-app/app/rider/ride-history.tsx +90 -0
  147. package/examples/uber-app/app/rider/ride-tracking.tsx +108 -0
  148. package/examples/uber-app/components/DriverCard.tsx +176 -0
  149. package/examples/uber-app/components/MapView.tsx +216 -0
  150. package/examples/uber-app/components/RatingStars.tsx +227 -0
  151. package/examples/uber-app/components/RideCard.tsx +167 -0
  152. package/examples/uber-app/mm.config.ts +30 -0
  153. package/examples/uber-app/models/location.model.ts +70 -0
  154. package/examples/uber-app/models/payment.model.ts +87 -0
  155. package/examples/uber-app/models/rating.model.ts +54 -0
  156. package/examples/uber-app/models/ride.model.ts +118 -0
  157. package/examples/uber-app/models/user.model.ts +66 -0
  158. package/examples/uber-app/models/vehicle.model.ts +63 -0
  159. package/examples/uber-app/tests/payment.test.tsx +129 -0
  160. package/examples/uber-app/tests/ride-flow.test.tsx +123 -0
  161. package/examples/uber-app/workflows/dispute-resolution.workflow.tsx +205 -0
  162. package/examples/uber-app/workflows/driver-onboarding.workflow.tsx +227 -0
  163. package/examples/uber-app/workflows/payment-processing.workflow.tsx +223 -0
  164. package/examples/uber-app/workflows/ride-request.workflow.tsx +194 -0
  165. package/package.json +77 -0
  166. package/package.json.backup +86 -0
  167. package/scripts/decompile.ts +226 -0
  168. package/scripts/seed-auth.ts +267 -0
  169. package/scripts/seed-uber.ts +248 -0
  170. package/scripts/validate-uber.ts +119 -0
  171. package/seed-blueprint-chat.mjs +444 -0
  172. package/seed-blueprint-glass-console.mjs +445 -0
  173. package/seed-compiled.mjs +318 -0
  174. package/src/RoundTripValidator.ts +400 -0
  175. package/src/__tests__/atom-rendering-coverage.test.ts +680 -0
  176. package/src/__tests__/auth-module-compilation.test.ts +247 -0
  177. package/src/__tests__/auth-template-compilation.test.ts +589 -0
  178. package/src/__tests__/change-extractor.test.ts +142 -0
  179. package/src/__tests__/cli-pull.test.ts +73 -0
  180. package/src/__tests__/cli-test.test.ts +72 -0
  181. package/src/__tests__/component-extractor.test.ts +331 -0
  182. package/src/__tests__/context-extractor.test.ts +145 -0
  183. package/src/__tests__/decompiler.test.ts +718 -0
  184. package/src/__tests__/define-blueprint.test.ts +133 -0
  185. package/src/__tests__/definition-validator.test.ts +519 -0
  186. package/src/__tests__/during-extractor.test.ts +152 -0
  187. package/src/__tests__/effect-extractor.test.ts +107 -0
  188. package/src/__tests__/event-emission.test.ts +127 -0
  189. package/src/__tests__/examples.test.ts +236 -0
  190. package/src/__tests__/full-blueprint-coverage.test.ts +1221 -0
  191. package/src/__tests__/golden-suite.test.ts +403 -0
  192. package/src/__tests__/grammar-island-extractor.test.ts +289 -0
  193. package/src/__tests__/instance-key.test.ts +82 -0
  194. package/src/__tests__/ir-migration.test.ts +255 -0
  195. package/src/__tests__/lock-file.test.ts +117 -0
  196. package/src/__tests__/model-extractor.test.ts +195 -0
  197. package/src/__tests__/model-field-acl.test.ts +237 -0
  198. package/src/__tests__/model-hooks.test.ts +130 -0
  199. package/src/__tests__/model-ref-resolution.test.ts +268 -0
  200. package/src/__tests__/model-roundtrip.test.ts +502 -0
  201. package/src/__tests__/model-runtime.test.ts +112 -0
  202. package/src/__tests__/model-transitions.test.ts +183 -0
  203. package/src/__tests__/nrt-action-trace.test.ts +391 -0
  204. package/src/__tests__/pipeline-hardening.test.ts +413 -0
  205. package/src/__tests__/project-compiler.test.ts +546 -0
  206. package/src/__tests__/project-decompiler.test.ts +343 -0
  207. package/src/__tests__/query-compilation.test.ts +145 -0
  208. package/src/__tests__/round-trip/PLAN.md +158 -0
  209. package/src/__tests__/round-trip/README.md +52 -0
  210. package/src/__tests__/round-trip/RESULTS.md +86 -0
  211. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +55 -0
  212. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +11 -0
  213. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +54 -0
  214. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +79 -0
  215. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +12 -0
  216. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +50 -0
  217. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +25 -0
  218. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +11 -0
  219. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +32 -0
  220. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +79 -0
  221. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +10 -0
  222. package/src/__tests__/round-trip/round-trip.test.ts +2598 -0
  223. package/src/__tests__/round-trip-ir.test.ts +300 -0
  224. package/src/__tests__/round-trip.test.ts +1212 -0
  225. package/src/__tests__/route-merging.test.ts +372 -0
  226. package/src/__tests__/router-composition.test.ts +489 -0
  227. package/src/__tests__/router-extractor.test.ts +176 -0
  228. package/src/__tests__/server-action-extractor.test.ts +128 -0
  229. package/src/__tests__/smart-type-inference.test.ts +365 -0
  230. package/src/__tests__/source-envelope.test.ts +284 -0
  231. package/src/__tests__/source-fidelity.test.ts +516 -0
  232. package/src/__tests__/state-extractor.test.ts +115 -0
  233. package/src/__tests__/strict-mode.test.ts +227 -0
  234. package/src/__tests__/transition-effect-extractor.test.ts +119 -0
  235. package/src/__tests__/transition-extractor.test.ts +68 -0
  236. package/src/__tests__/ts-to-expression.test.ts +462 -0
  237. package/src/__tests__/type-generator.test.ts +201 -0
  238. package/src/__tests__/uber-validation.test.ts +502 -0
  239. package/src/action-compiler.ts +361 -0
  240. package/src/babel/emitters/experience-transform.ts +199 -0
  241. package/src/babel/emitters/ir-to-tsx-emitter.ts +110 -0
  242. package/src/babel/emitters/pure-form-emitter.ts +1023 -0
  243. package/src/babel/emitters/runtime-glue-emitter.ts +39 -0
  244. package/src/babel/extractors/change-extractor.ts +199 -0
  245. package/src/babel/extractors/component-extractor.ts +907 -0
  246. package/src/babel/extractors/computed-extractor.ts +262 -0
  247. package/src/babel/extractors/context-extractor.ts +277 -0
  248. package/src/babel/extractors/during-extractor.ts +295 -0
  249. package/src/babel/extractors/effect-extractor.ts +340 -0
  250. package/src/babel/extractors/event-extractor.ts +235 -0
  251. package/src/babel/extractors/grammar-island-extractor.ts +302 -0
  252. package/src/babel/extractors/model-extractor.ts +1018 -0
  253. package/src/babel/extractors/router-extractor.ts +303 -0
  254. package/src/babel/extractors/server-action-extractor.ts +173 -0
  255. package/src/babel/extractors/server-action-hook-extractor.ts +72 -0
  256. package/src/babel/extractors/server-state-extractor.ts +88 -0
  257. package/src/babel/extractors/state-extractor.ts +214 -0
  258. package/src/babel/extractors/transition-effect-extractor.ts +176 -0
  259. package/src/babel/extractors/transition-extractor.ts +143 -0
  260. package/src/babel/index.ts +24 -0
  261. package/src/babel/transpilers/ts-to-expression.ts +674 -0
  262. package/src/babel/visitor.ts +807 -0
  263. package/src/cli/auth.ts +255 -0
  264. package/src/cli/build.ts +288 -0
  265. package/src/cli/deploy.ts +206 -0
  266. package/src/cli/index.ts +328 -0
  267. package/src/cli/init.ts +388 -0
  268. package/src/cli/installer.ts +261 -0
  269. package/src/cli/lock-file.ts +94 -0
  270. package/src/cli/mmrc.ts +22 -0
  271. package/src/cli/pull.ts +172 -0
  272. package/src/cli/registry-client.ts +175 -0
  273. package/src/cli/test.ts +397 -0
  274. package/src/cli/type-generator.ts +243 -0
  275. package/src/codemod/__tests__/forward.test.ts +239 -0
  276. package/src/codemod/__tests__/reverse.test.ts +145 -0
  277. package/src/codemod/__tests__/round-trip.test.ts +137 -0
  278. package/src/codemod/annotation.ts +97 -0
  279. package/src/codemod/classify.ts +197 -0
  280. package/src/codemod/cli.ts +207 -0
  281. package/src/codemod/control-flow.ts +409 -0
  282. package/src/codemod/forward.ts +244 -0
  283. package/src/codemod/import-manager.ts +171 -0
  284. package/src/codemod/index.ts +120 -0
  285. package/src/codemod/reverse.ts +197 -0
  286. package/src/codemod/rules.ts +174 -0
  287. package/src/codemod/state-transform.ts +126 -0
  288. package/src/decompiler/ast-builder.ts +538 -0
  289. package/src/decompiler/config-generator.ts +151 -0
  290. package/src/decompiler/index.ts +315 -0
  291. package/src/decompiler/project-decompiler.ts +1776 -0
  292. package/src/decompiler/project.ts +862 -0
  293. package/src/decompiler/split-strategy.ts +140 -0
  294. package/src/decompiler/state-emitter.ts +1053 -0
  295. package/src/decompiler/sx-emitter.ts +318 -0
  296. package/src/decompiler/workspace-hydrator.ts +189 -0
  297. package/src/dev-server.ts +238 -0
  298. package/src/envelope/fs-tree.ts +217 -0
  299. package/src/envelope/source-envelope.ts +264 -0
  300. package/src/envelope.ts +315 -0
  301. package/src/incremental-compiler.ts +401 -0
  302. package/src/index.ts +99 -0
  303. package/src/model-compiler.ts +277 -0
  304. package/src/project-compiler.ts +1629 -0
  305. package/src/route-extractor.ts +333 -0
  306. package/src/testing/index.ts +32 -0
  307. package/src/testing/snapshot.ts +252 -0
  308. package/src/testing/test-utils.ts +226 -0
  309. package/src/types.ts +68 -0
  310. package/src/vite/index.ts +288 -0
  311. package/test-compile.mjs +142 -0
  312. package/tsconfig.json +25 -0
  313. package/tsup.config.ts +23 -0
  314. package/vitest.config.ts +9 -0
@@ -0,0 +1,197 @@
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
+ }
@@ -0,0 +1,207 @@
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
+ });