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

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 (346) hide show
  1. package/ATOM-PIPELINE.md +144 -0
  2. package/README.md +88 -40
  3. package/dist/auth-3UK75242.mjs +17 -0
  4. package/dist/babel/index.d.mts +2 -2
  5. package/dist/babel/index.d.ts +2 -2
  6. package/dist/babel/index.js +2816 -279
  7. package/dist/babel/index.mjs +2 -2
  8. package/dist/chunk-3USIFFE4.mjs +2190 -0
  9. package/dist/chunk-45YMGEVT.mjs +186 -0
  10. package/dist/chunk-4FN2AISW.mjs +148 -0
  11. package/dist/chunk-4OPI5L7G.mjs +2593 -0
  12. package/dist/chunk-4RYTKOOJ.mjs +186 -0
  13. package/dist/chunk-52XHYD2V.mjs +214 -0
  14. package/dist/chunk-5FTDWKHH.mjs +244 -0
  15. package/dist/chunk-5GUFFFGL.mjs +148 -0
  16. package/dist/chunk-5RKTOVR5.mjs +244 -0
  17. package/dist/chunk-5YDMOO4X.mjs +214 -0
  18. package/dist/chunk-64ZWEMLJ.mjs +148 -0
  19. package/dist/chunk-6XP4KSWQ.mjs +2190 -0
  20. package/dist/chunk-72QWL54I.mjs +175 -0
  21. package/dist/chunk-7B4TRI7C.mjs +4835 -0
  22. package/dist/chunk-7JRAEFRB.mjs +7510 -0
  23. package/dist/chunk-7T6Q5KAA.mjs +7506 -0
  24. package/dist/chunk-7ZKGHTNB.mjs +4952 -0
  25. package/dist/chunk-ABYPKRSB.mjs +215 -0
  26. package/dist/chunk-BZEXUPDH.mjs +175 -0
  27. package/dist/chunk-CIESM3BP.mjs +33 -0
  28. package/dist/chunk-DE3ZGQAC.mjs +148 -0
  29. package/dist/chunk-DMCY3BBG.mjs +1933 -0
  30. package/dist/chunk-DPIK3PJS.mjs +244 -0
  31. package/dist/chunk-E5IVH4RE.mjs +186 -0
  32. package/dist/chunk-E6FZNUR5.mjs +4953 -0
  33. package/dist/chunk-EJRBDQDP.mjs +2607 -0
  34. package/dist/chunk-ELO4TXJL.mjs +186 -0
  35. package/dist/chunk-EO6SYNCG.mjs +175 -0
  36. package/dist/chunk-FKRO52XH.mjs +3446 -0
  37. package/dist/chunk-FL4YAKU6.mjs +4941 -0
  38. package/dist/chunk-FYT47UBU.mjs +5076 -0
  39. package/dist/chunk-GCLGPOJZ.mjs +148 -0
  40. package/dist/chunk-GXB4JOP7.mjs +5072 -0
  41. package/dist/chunk-HFXOUMTD.mjs +175 -0
  42. package/dist/chunk-HRYR54PT.mjs +175 -0
  43. package/dist/chunk-HWIZ47US.mjs +214 -0
  44. package/dist/chunk-IB7MNPQL.mjs +4953 -0
  45. package/dist/chunk-ICSIHQCG.mjs +148 -0
  46. package/dist/chunk-J3M4GUS7.mjs +161 -0
  47. package/dist/chunk-J7JUAHS4.mjs +186 -0
  48. package/dist/chunk-JLA5VNQ3.mjs +186 -0
  49. package/dist/chunk-JQLWFCTM.mjs +214 -0
  50. package/dist/chunk-JRGFBWTN.mjs +2918 -0
  51. package/dist/chunk-KFJJCQAL.mjs +148 -0
  52. package/dist/chunk-KJUIIEQE.mjs +186 -0
  53. package/dist/chunk-KNWTHRVQ.mjs +175 -0
  54. package/dist/chunk-KSG4XSZF.mjs +175 -0
  55. package/dist/chunk-LF5N6DOU.mjs +175 -0
  56. package/dist/chunk-LJQCM2IM.mjs +214 -0
  57. package/dist/chunk-NTB7OEX2.mjs +2918 -0
  58. package/dist/chunk-NW6555WJ.mjs +186 -0
  59. package/dist/chunk-O4AUS7EU.mjs +148 -0
  60. package/dist/chunk-OMZE6VLQ.mjs +214 -0
  61. package/dist/chunk-OPJKP747.mjs +7506 -0
  62. package/dist/chunk-P4BR7WVO.mjs +2190 -0
  63. package/dist/chunk-QQHVYH2X.mjs +244 -0
  64. package/dist/chunk-R2DD5GTY.mjs +186 -0
  65. package/dist/chunk-S5QLWLLT.mjs +186 -0
  66. package/dist/chunk-SCWGT2FY.mjs +2190 -0
  67. package/dist/chunk-SMKJUSB3.mjs +2190 -0
  68. package/dist/chunk-THFYE5ZX.mjs +244 -0
  69. package/dist/chunk-UDDTWG5J.mjs +734 -0
  70. package/dist/chunk-VCAY2KGM.mjs +175 -0
  71. package/dist/chunk-VLTKQDJ3.mjs +244 -0
  72. package/dist/chunk-WBYMW4NQ.mjs +3450 -0
  73. package/dist/chunk-WECAV6QB.mjs +148 -0
  74. package/dist/chunk-WMKBXUCE.mjs +3228 -0
  75. package/dist/chunk-WVYY32LD.mjs +939 -0
  76. package/dist/chunk-XAJ5BKKL.mjs +4947 -0
  77. package/dist/chunk-XDVM4YHX.mjs +3450 -0
  78. package/dist/chunk-XG2X7AEA.mjs +175 -0
  79. package/dist/chunk-XG7Z23NQ.mjs +148 -0
  80. package/dist/chunk-XWZAOCQ7.mjs +2607 -0
  81. package/dist/chunk-Y6MA7ULW.mjs +148 -0
  82. package/dist/chunk-YMS7Q7LG.mjs +214 -0
  83. package/dist/chunk-Z2G5RZ4H.mjs +186 -0
  84. package/dist/chunk-ZA37XTGA.mjs +175 -0
  85. package/dist/chunk-ZE3KCHBM.mjs +2918 -0
  86. package/dist/cli/index.js +14720 -7199
  87. package/dist/cli/index.mjs +224 -183
  88. package/dist/codemod/cli.js +1 -1
  89. package/dist/codemod/cli.mjs +2 -2
  90. package/dist/codemod/index.d.mts +3 -3
  91. package/dist/codemod/index.d.ts +3 -3
  92. package/dist/codemod/index.js +1 -1
  93. package/dist/codemod/index.mjs +2 -2
  94. package/dist/config-PL24KEWL.mjs +219 -0
  95. package/dist/deploy-YAJGW6II.mjs +9 -0
  96. package/dist/dev-server-CrQ041KP.d.mts +79 -0
  97. package/dist/dev-server-CrQ041KP.d.ts +79 -0
  98. package/dist/dev-server-RmGHIntF.d.mts +113 -0
  99. package/dist/dev-server-RmGHIntF.d.ts +113 -0
  100. package/dist/dev-server.d.mts +2 -2
  101. package/dist/dev-server.d.ts +2 -2
  102. package/dist/dev-server.js +6424 -1597
  103. package/dist/dev-server.mjs +5 -5
  104. package/dist/envelope-ChEkuHij.d.mts +265 -0
  105. package/dist/envelope-ChEkuHij.d.ts +265 -0
  106. package/dist/envelope.d.mts +2 -2
  107. package/dist/envelope.d.ts +2 -2
  108. package/dist/envelope.js +2814 -277
  109. package/dist/envelope.mjs +3 -3
  110. package/dist/index-CEKyyazf.d.mts +104 -0
  111. package/dist/index-CEKyyazf.d.ts +104 -0
  112. package/dist/index.d.mts +168 -9
  113. package/dist/index.d.ts +168 -9
  114. package/dist/index.js +5606 -681
  115. package/dist/index.mjs +217 -9
  116. package/dist/init-7FJENUDK.mjs +407 -0
  117. package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
  118. package/dist/init-DQDX3QK6.mjs +369 -0
  119. package/dist/init-EHO4VQ22.mjs +369 -0
  120. package/dist/init-UC3FWPIW.mjs +367 -0
  121. package/dist/init-UNSMVKIK.mjs +366 -0
  122. package/dist/init-UNV5XIDE.mjs +367 -0
  123. package/dist/project-compiler-2P4N4DR7.mjs +10 -0
  124. package/dist/project-compiler-D2LCC27O.mjs +10 -0
  125. package/dist/project-compiler-EJ3GANJE.mjs +10 -0
  126. package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
  127. package/dist/project-compiler-NNK32MPG.mjs +10 -0
  128. package/dist/project-compiler-OP2VVGJQ.mjs +10 -0
  129. package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
  130. package/dist/project-compiler-VWNNCHGO.mjs +10 -0
  131. package/dist/project-compiler-XVAAU4C5.mjs +10 -0
  132. package/dist/project-compiler-YES5FGMD.mjs +10 -0
  133. package/dist/project-compiler-ZB4RUYVL.mjs +10 -0
  134. package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
  135. package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
  136. package/dist/project-decompiler-U55HQUHW.mjs +7 -0
  137. package/dist/project-decompiler-US7GAVIC.mjs +7 -0
  138. package/dist/project-decompiler-VLPR22QF.mjs +7 -0
  139. package/dist/pull-FUS5QYZS.mjs +109 -0
  140. package/dist/pull-KOL2QAYQ.mjs +109 -0
  141. package/dist/pull-LD5ENLGY.mjs +109 -0
  142. package/dist/pull-P44LDRWB.mjs +109 -0
  143. package/dist/seed-KOGEPGOJ.mjs +154 -0
  144. package/dist/server-VW6UPCHO.mjs +277 -0
  145. package/dist/testing/index.d.mts +8 -8
  146. package/dist/testing/index.d.ts +8 -8
  147. package/dist/testing/index.js +2824 -287
  148. package/dist/testing/index.mjs +2 -2
  149. package/dist/verify-BYHUKARQ.mjs +1833 -0
  150. package/dist/verify-OQDEQYMS.mjs +1833 -0
  151. package/dist/verify-SEIXUGN4.mjs +1833 -0
  152. package/dist/vite/index.d.mts +1 -1
  153. package/dist/vite/index.d.ts +1 -1
  154. package/dist/vite/index.js +2817 -280
  155. package/dist/vite/index.mjs +3 -3
  156. package/examples/authentication/main.workflow.tsx +1 -1
  157. package/examples/authentication/mm.config.ts +1 -1
  158. package/examples/authentication/pages/LoginPage.tsx +2 -2
  159. package/examples/authentication/pages/SignupPage.tsx +2 -2
  160. package/examples/counter.workflow.tsx +1 -1
  161. package/examples/dashboard.workflow.tsx +1 -1
  162. package/examples/invoice-approval/actions/invoice.server.ts +1 -1
  163. package/examples/invoice-approval/main.workflow.tsx +1 -1
  164. package/examples/invoice-approval/mm.config.ts +1 -1
  165. package/examples/invoice-approval/pages/InvoiceDetailPage.tsx +1 -1
  166. package/examples/invoice-approval/pages/InvoiceFormPage.tsx +1 -1
  167. package/examples/invoice-approval/pages/InvoiceListPage.tsx +1 -1
  168. package/examples/todo-app.workflow.tsx +1 -1
  169. package/examples/uber-app/actions/matching.server.ts +1 -1
  170. package/examples/uber-app/actions/notifications.server.ts +1 -1
  171. package/examples/uber-app/actions/payments.server.ts +1 -1
  172. package/examples/uber-app/actions/pricing.server.ts +1 -1
  173. package/examples/uber-app/app/admin/analytics.tsx +2 -2
  174. package/examples/uber-app/app/admin/fleet.tsx +21 -21
  175. package/examples/uber-app/app/admin/surge-pricing.tsx +2 -2
  176. package/examples/uber-app/app/driver/dashboard.tsx +2 -2
  177. package/examples/uber-app/app/driver/earnings.tsx +2 -2
  178. package/examples/uber-app/app/driver/navigation.tsx +2 -2
  179. package/examples/uber-app/app/driver/ride-acceptance.tsx +2 -2
  180. package/examples/uber-app/app/rider/home.tsx +2 -2
  181. package/examples/uber-app/app/rider/payment-methods.tsx +2 -2
  182. package/examples/uber-app/app/rider/ride-history.tsx +2 -2
  183. package/examples/uber-app/app/rider/ride-tracking.tsx +2 -2
  184. package/examples/uber-app/components/DriverCard.tsx +1 -1
  185. package/examples/uber-app/components/MapView.tsx +3 -3
  186. package/examples/uber-app/components/RatingStars.tsx +2 -2
  187. package/examples/uber-app/components/RideCard.tsx +1 -1
  188. package/examples/uber-app/mm.config.ts +1 -1
  189. package/examples/uber-app/workflows/dispute-resolution.workflow.tsx +2 -2
  190. package/examples/uber-app/workflows/driver-onboarding.workflow.tsx +2 -2
  191. package/examples/uber-app/workflows/payment-processing.workflow.tsx +2 -2
  192. package/examples/uber-app/workflows/ride-request.workflow.tsx +2 -2
  193. package/package.json +10 -4
  194. package/compile-blueprint-chat.mjs +0 -99
  195. package/compile-blueprint-glass-console.mjs +0 -98
  196. package/compile-chat-defs.mjs +0 -92
  197. package/examples/uber-app/tests/payment.test.tsx +0 -129
  198. package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
  199. package/package.json.backup +0 -86
  200. package/scripts/decompile.ts +0 -226
  201. package/scripts/seed-auth.ts +0 -267
  202. package/scripts/seed-uber.ts +0 -248
  203. package/scripts/validate-uber.ts +0 -119
  204. package/seed-blueprint-chat.mjs +0 -444
  205. package/seed-blueprint-glass-console.mjs +0 -445
  206. package/seed-compiled.mjs +0 -318
  207. package/src/RoundTripValidator.ts +0 -400
  208. package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
  209. package/src/__tests__/auth-module-compilation.test.ts +0 -247
  210. package/src/__tests__/auth-template-compilation.test.ts +0 -589
  211. package/src/__tests__/change-extractor.test.ts +0 -142
  212. package/src/__tests__/cli-pull.test.ts +0 -73
  213. package/src/__tests__/cli-test.test.ts +0 -72
  214. package/src/__tests__/component-extractor.test.ts +0 -331
  215. package/src/__tests__/context-extractor.test.ts +0 -145
  216. package/src/__tests__/decompiler.test.ts +0 -718
  217. package/src/__tests__/define-blueprint.test.ts +0 -133
  218. package/src/__tests__/definition-validator.test.ts +0 -519
  219. package/src/__tests__/during-extractor.test.ts +0 -152
  220. package/src/__tests__/effect-extractor.test.ts +0 -107
  221. package/src/__tests__/event-emission.test.ts +0 -127
  222. package/src/__tests__/examples.test.ts +0 -236
  223. package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
  224. package/src/__tests__/golden-suite.test.ts +0 -403
  225. package/src/__tests__/grammar-island-extractor.test.ts +0 -289
  226. package/src/__tests__/instance-key.test.ts +0 -82
  227. package/src/__tests__/ir-migration.test.ts +0 -255
  228. package/src/__tests__/lock-file.test.ts +0 -117
  229. package/src/__tests__/model-extractor.test.ts +0 -195
  230. package/src/__tests__/model-field-acl.test.ts +0 -237
  231. package/src/__tests__/model-hooks.test.ts +0 -130
  232. package/src/__tests__/model-ref-resolution.test.ts +0 -268
  233. package/src/__tests__/model-roundtrip.test.ts +0 -502
  234. package/src/__tests__/model-runtime.test.ts +0 -112
  235. package/src/__tests__/model-transitions.test.ts +0 -183
  236. package/src/__tests__/nrt-action-trace.test.ts +0 -391
  237. package/src/__tests__/pipeline-hardening.test.ts +0 -413
  238. package/src/__tests__/project-compiler.test.ts +0 -546
  239. package/src/__tests__/project-decompiler.test.ts +0 -343
  240. package/src/__tests__/query-compilation.test.ts +0 -145
  241. package/src/__tests__/round-trip/PLAN.md +0 -158
  242. package/src/__tests__/round-trip/README.md +0 -52
  243. package/src/__tests__/round-trip/RESULTS.md +0 -86
  244. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
  245. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
  246. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
  247. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
  248. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
  249. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
  250. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
  251. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
  252. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
  253. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
  254. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
  255. package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
  256. package/src/__tests__/round-trip-ir.test.ts +0 -300
  257. package/src/__tests__/round-trip.test.ts +0 -1212
  258. package/src/__tests__/route-merging.test.ts +0 -372
  259. package/src/__tests__/router-composition.test.ts +0 -489
  260. package/src/__tests__/router-extractor.test.ts +0 -176
  261. package/src/__tests__/server-action-extractor.test.ts +0 -128
  262. package/src/__tests__/smart-type-inference.test.ts +0 -365
  263. package/src/__tests__/source-envelope.test.ts +0 -284
  264. package/src/__tests__/source-fidelity.test.ts +0 -516
  265. package/src/__tests__/state-extractor.test.ts +0 -115
  266. package/src/__tests__/strict-mode.test.ts +0 -227
  267. package/src/__tests__/transition-effect-extractor.test.ts +0 -119
  268. package/src/__tests__/transition-extractor.test.ts +0 -68
  269. package/src/__tests__/ts-to-expression.test.ts +0 -462
  270. package/src/__tests__/type-generator.test.ts +0 -201
  271. package/src/__tests__/uber-validation.test.ts +0 -502
  272. package/src/action-compiler.ts +0 -361
  273. package/src/babel/emitters/experience-transform.ts +0 -199
  274. package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
  275. package/src/babel/emitters/pure-form-emitter.ts +0 -1023
  276. package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
  277. package/src/babel/extractors/change-extractor.ts +0 -199
  278. package/src/babel/extractors/component-extractor.ts +0 -907
  279. package/src/babel/extractors/computed-extractor.ts +0 -262
  280. package/src/babel/extractors/context-extractor.ts +0 -277
  281. package/src/babel/extractors/during-extractor.ts +0 -295
  282. package/src/babel/extractors/effect-extractor.ts +0 -340
  283. package/src/babel/extractors/event-extractor.ts +0 -235
  284. package/src/babel/extractors/grammar-island-extractor.ts +0 -302
  285. package/src/babel/extractors/model-extractor.ts +0 -1018
  286. package/src/babel/extractors/router-extractor.ts +0 -303
  287. package/src/babel/extractors/server-action-extractor.ts +0 -173
  288. package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
  289. package/src/babel/extractors/server-state-extractor.ts +0 -88
  290. package/src/babel/extractors/state-extractor.ts +0 -214
  291. package/src/babel/extractors/transition-effect-extractor.ts +0 -176
  292. package/src/babel/extractors/transition-extractor.ts +0 -143
  293. package/src/babel/index.ts +0 -24
  294. package/src/babel/transpilers/ts-to-expression.ts +0 -674
  295. package/src/babel/visitor.ts +0 -807
  296. package/src/cli/auth.ts +0 -255
  297. package/src/cli/build.ts +0 -288
  298. package/src/cli/deploy.ts +0 -206
  299. package/src/cli/index.ts +0 -328
  300. package/src/cli/installer.ts +0 -261
  301. package/src/cli/lock-file.ts +0 -94
  302. package/src/cli/mmrc.ts +0 -22
  303. package/src/cli/pull.ts +0 -172
  304. package/src/cli/registry-client.ts +0 -175
  305. package/src/cli/test.ts +0 -397
  306. package/src/cli/type-generator.ts +0 -243
  307. package/src/codemod/__tests__/forward.test.ts +0 -239
  308. package/src/codemod/__tests__/reverse.test.ts +0 -145
  309. package/src/codemod/__tests__/round-trip.test.ts +0 -137
  310. package/src/codemod/annotation.ts +0 -97
  311. package/src/codemod/classify.ts +0 -197
  312. package/src/codemod/cli.ts +0 -207
  313. package/src/codemod/control-flow.ts +0 -409
  314. package/src/codemod/forward.ts +0 -244
  315. package/src/codemod/import-manager.ts +0 -171
  316. package/src/codemod/index.ts +0 -120
  317. package/src/codemod/reverse.ts +0 -197
  318. package/src/codemod/rules.ts +0 -174
  319. package/src/codemod/state-transform.ts +0 -126
  320. package/src/decompiler/ast-builder.ts +0 -538
  321. package/src/decompiler/config-generator.ts +0 -151
  322. package/src/decompiler/index.ts +0 -315
  323. package/src/decompiler/project-decompiler.ts +0 -1776
  324. package/src/decompiler/project.ts +0 -862
  325. package/src/decompiler/split-strategy.ts +0 -140
  326. package/src/decompiler/state-emitter.ts +0 -1053
  327. package/src/decompiler/sx-emitter.ts +0 -318
  328. package/src/decompiler/workspace-hydrator.ts +0 -189
  329. package/src/dev-server.ts +0 -238
  330. package/src/envelope/fs-tree.ts +0 -217
  331. package/src/envelope/source-envelope.ts +0 -264
  332. package/src/envelope.ts +0 -315
  333. package/src/incremental-compiler.ts +0 -401
  334. package/src/index.ts +0 -99
  335. package/src/model-compiler.ts +0 -277
  336. package/src/project-compiler.ts +0 -1629
  337. package/src/route-extractor.ts +0 -333
  338. package/src/testing/index.ts +0 -32
  339. package/src/testing/snapshot.ts +0 -252
  340. package/src/testing/test-utils.ts +0 -226
  341. package/src/types.ts +0 -68
  342. package/src/vite/index.ts +0 -288
  343. package/test-compile.mjs +0 -142
  344. package/tsconfig.json +0 -25
  345. package/tsup.config.ts +0 -23
  346. package/vitest.config.ts +0 -9
@@ -1,1629 +0,0 @@
1
- /**
2
- * ProjectCompiler — multi-file project compilation for UBER-SCALE apps.
3
- *
4
- * Takes a Record<string, string> of filename → source code, compiles each
5
- * file to IR independently, then composes a blueprint with child definitions.
6
- *
7
- * Handles:
8
- * - Multiple .workflow.tsx files → each becomes a child IRWorkflowDefinition
9
- * - Model files (models/*.ts) → category='data' child definitions
10
- * - Server action files (*.server.ts) → registered action metadata
11
- * - Page files (pages/ or app/ dir .tsx) → route table + router workflow child
12
- * - mm.config.ts → parent blueprint metadata
13
- * - Cross-file imports (topological sort for compilation order)
14
- * - Incremental compilation (hash-based, only recompile changed files)
15
- *
16
- * Phase 2 enhancements:
17
- * - Model compilation via model-compiler module
18
- * - Route extraction via route-extractor module
19
- * - Action compilation via action-compiler module
20
- * - Incremental compilation via incremental-compiler module
21
- * - Cross-file import resolution with data source linking
22
- */
23
-
24
- import { transformSync } from '@babel/core';
25
- import type {
26
- IRWorkflowDefinition,
27
- IRFieldDefinition,
28
- IRStateDefinition,
29
- IRTransitionDefinition,
30
- IROnEventSubscription,
31
- IRExperienceNode,
32
- IRGrammarIsland,
33
- } from '@mindmatrix/player-core';
34
- import babelPlugin from './babel';
35
- import type { ReactCompilerError } from './types';
36
- import {
37
- extractRouterWorkflow,
38
- pathToStateName,
39
- pathToUrlPattern,
40
- extractParams,
41
- } from './babel/extractors/router-extractor';
42
- import type { PageFile } from './babel/extractors/router-extractor';
43
- import type { ServerAction } from './babel/extractors/server-action-extractor';
44
-
45
- // Phase 2 module imports
46
- import { compileModels } from './model-compiler';
47
- import type { ModelCompilationResult } from './model-compiler';
48
- import { extractRoutes } from './route-extractor';
49
- import type { RouteExtractionResult } from './route-extractor';
50
- import { compileActions } from './action-compiler';
51
- import type { ActionCompilationResult } from './action-compiler';
52
- import {
53
- hashContent,
54
- resolveImport,
55
- buildDependencyGraph,
56
- topologicalSort,
57
- IncrementalCache,
58
- } from './incremental-compiler';
59
- import type { IncrementalStats } from './incremental-compiler';
60
-
61
- // =============================================================================
62
- // Types
63
- // =============================================================================
64
-
65
- /**
66
- * A resolved module's compilation output, used for route merging.
67
- * The caller (build tool) compiles each dependency and passes the result here.
68
- */
69
- export interface ResolvedModule {
70
- /** The module's slug (must match the dependency slug in mm.config.ts). */
71
- slug: string;
72
- /** The module's compiled route table. */
73
- routeTable: RouteTableEntry[];
74
- /** The module's declared routes from its mm.config.ts (if any). */
75
- manifestRoutes?: Array<{ path: string; label?: string; group?: string; icon?: string; showInNav?: boolean }>;
76
- }
77
-
78
- export interface ProjectCompilerOptions {
79
- /** Compilation mode: strict or infer. Default: 'infer'. */
80
- mode?: 'strict' | 'infer';
81
- /** Enable Phase 2 modules (model-compiler, route-extractor, action-compiler). Default: true. */
82
- usePhase2Modules?: boolean;
83
- /** Pre-compiled module results for route merging at compile time. */
84
- resolvedModules?: ResolvedModule[];
85
- }
86
-
87
- export interface ProjectCompilationResult {
88
- /** The parent blueprint IRWorkflowDefinition (merged from all files). */
89
- ir: IRWorkflowDefinition;
90
- /** Child workflow definitions — one per .workflow.tsx + one per model + router. */
91
- childDefinitions: IRWorkflowDefinition[];
92
- /** Per-file IRs keyed by filename. */
93
- fileIRs: Record<string, IRWorkflowDefinition>;
94
- /** Route table extracted from pages*.tsx directory structure. */
95
- routeTable: RouteTableEntry[];
96
- /** Server actions extracted from *.server.ts files. */
97
- serverActions: ServerActionEntry[];
98
- /** Errors from individual file compilations. */
99
- errors: ProjectCompilationError[];
100
- /** Warnings from individual file compilations. */
101
- warnings: ProjectCompilationError[];
102
- /** Per-page experience trees extracted from app/**\/*.tsx file IRs. */
103
- pageExperiences: Record<string, IRExperienceNode>;
104
- /** Component definitions captured from components/*.tsx files. */
105
- componentDefinitions: Record<string, { experience: IRExperienceNode; props: string[] }>;
106
- /** Phase 2: Cross-file import links (importer → imported data sources). */
107
- importLinks?: ImportLink[];
108
- /** Phase 2: Model compilation results (if usePhase2Modules enabled). */
109
- modelResults?: Map<string, ModelCompilationResult>;
110
- /** Phase 2: Action compilation result (if usePhase2Modules enabled). */
111
- actionResult?: ActionCompilationResult;
112
- /** Phase 2: Route extraction result (if usePhase2Modules enabled). */
113
- routeResult?: RouteExtractionResult;
114
- }
115
-
116
- export interface ProjectCompilationError {
117
- file: string;
118
- message: string;
119
- line?: number;
120
- column?: number;
121
- /** End line for marker spans (defaults to start line). */
122
- endLine?: number;
123
- /** End column for marker spans (defaults to start column + token length). */
124
- endColumn?: number;
125
- severity: 'error' | 'warning';
126
- }
127
-
128
- export interface RouteTableEntry {
129
- /** Route URL path (e.g., '/rider/home'). */
130
- path: string;
131
- /** State name derived from the route (e.g., 'RIDER_HOME'). */
132
- stateName: string;
133
- /** Source file path (e.g., 'pages/rider/home.tsx'). */
134
- sourceFile: string;
135
- /** Dynamic parameters from [param] segments. */
136
- params: string[];
137
- /** Module slug if this route was merged from a dependency. */
138
- moduleSlug?: string;
139
- }
140
-
141
- export interface ServerActionEntry {
142
- /** Function name. */
143
- name: string;
144
- /** Source file path. */
145
- sourceFile: string;
146
- /** Whether the function is async. */
147
- async: boolean;
148
- /** Parameter names. */
149
- params: string[];
150
- /** Description from JSDoc. */
151
- description?: string;
152
- /** Full function body source text (for round-trip preservation). */
153
- body?: string;
154
- /** TypeScript return type (e.g., 'Promise<ActionResult>'). */
155
- returnType?: string;
156
- }
157
-
158
- /** Phase 2: Cross-file import link metadata. */
159
- export interface ImportLink {
160
- /** The file that imports. */
161
- fromFile: string;
162
- /** The file being imported. */
163
- toFile: string;
164
- /** Type of link: 'data-source' for model imports, 'component' for component imports. */
165
- linkType: 'data-source' | 'component' | 'action' | 'unknown';
166
- /** Imported symbol names. */
167
- symbols: string[];
168
- }
169
-
170
- interface ParsedConfig {
171
- slug?: string;
172
- name?: string;
173
- version?: string;
174
- description?: string;
175
- category?: string | string[];
176
- mode?: 'strict' | 'infer';
177
- }
178
-
179
- // =============================================================================
180
- // Config Parser
181
- // =============================================================================
182
-
183
- /**
184
- * Parses mm.config.ts source to extract defineBlueprint() configuration.
185
- * Uses simple regex extraction rather than full evaluation since we
186
- * can't execute arbitrary TypeScript in a compiler context.
187
- */
188
- function parseConfig(source: string): ParsedConfig {
189
- const config: ParsedConfig = {};
190
-
191
- // Extract key-value pairs from defineBlueprint({ ... })
192
- const slugMatch = source.match(/slug:\s*['"]([^'"]+)['"]/);
193
- if (slugMatch) config.slug = slugMatch[1];
194
-
195
- const nameMatch = source.match(/name:\s*['"]([^'"]+)['"]/);
196
- if (nameMatch) config.name = nameMatch[1];
197
-
198
- const versionMatch = source.match(/version:\s*['"]([^'"]+)['"]/);
199
- if (versionMatch) config.version = versionMatch[1];
200
-
201
- const descMatch = source.match(/description:\s*['"]([^'"]+)['"]/);
202
- if (descMatch) config.description = descMatch[1];
203
-
204
- // Handle category as string or array: category: 'workflow' or category: ['workflow', 'blueprint', 'module']
205
- const categoryArrayMatch = source.match(/category:\s*\[([^\]]+)\]/);
206
- if (categoryArrayMatch) {
207
- const items = categoryArrayMatch[1].match(/['"]([^'"]+)['"]/g);
208
- if (items) {
209
- config.category = items.map(s => s.replace(/['"]/g, ''));
210
- }
211
- } else {
212
- const categoryMatch = source.match(/category:\s*['"]([^'"]+)['"]/);
213
- if (categoryMatch) config.category = categoryMatch[1];
214
- }
215
-
216
- const modeMatch = source.match(/mode:\s*['"]([^'"]+)['"]/);
217
- if (modeMatch && (modeMatch[1] === 'strict' || modeMatch[1] === 'infer')) {
218
- config.mode = modeMatch[1];
219
- }
220
-
221
- return config;
222
- }
223
-
224
- /**
225
- * Parses mm.module.ts source to extract defineModule() manifest as a JSON object.
226
- *
227
- * Uses regex extraction for scalar fields and array/object block extraction
228
- * for structured fields (models, routes, actions, slots, etc.).
229
- * Returns a plain object suitable for storing as metadata.module_manifest.
230
- */
231
- function parseModuleManifest(source: string): Record<string, unknown> | null {
232
- // Must contain defineBlueprint or defineModule to be a valid manifest
233
- if (!source.includes('defineBlueprint') && !source.includes('defineModule')) return null;
234
-
235
- const manifest: Record<string, unknown> = {};
236
-
237
- // Extract scalar string fields
238
- const stringFields = ['slug', 'name', 'version', 'description', 'author', 'license', 'icon'];
239
- for (const field of stringFields) {
240
- const match = source.match(new RegExp(`${field}:\\s*['"]([^'"]+)['"]`));
241
- if (match) manifest[field] = match[1];
242
- }
243
-
244
- // Handle category as string or array
245
- const catArrayMatch = source.match(/category:\s*\[([^\]]+)\]/);
246
- if (catArrayMatch) {
247
- const items = catArrayMatch[1].match(/['"]([^'"]+)['"]/g);
248
- if (items) {
249
- manifest.category = items.map((s: string) => s.replace(/['"]/g, ''));
250
- }
251
- } else {
252
- const catMatch = source.match(/category:\s*['"]([^'"]+)['"]/);
253
- if (catMatch) manifest.category = catMatch[1];
254
- }
255
-
256
- // Extract tags array
257
- const tagsMatch = source.match(/tags:\s*\[([^\]]*)\]/);
258
- if (tagsMatch) {
259
- manifest.tags = tagsMatch[1]
260
- .split(',')
261
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
262
- .filter(Boolean);
263
- }
264
-
265
- // Extract models array
266
- const modelsMatch = source.match(/models:\s*\[([^\]]*)\]/);
267
- if (modelsMatch) {
268
- manifest.models = modelsMatch[1]
269
- .split(',')
270
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
271
- .filter(Boolean);
272
- }
273
-
274
- // Extract capabilities array
275
- const capsMatch = source.match(/capabilities:\s*\[([^\]]*)\]/);
276
- if (capsMatch) {
277
- manifest.capabilities = capsMatch[1]
278
- .split(',')
279
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
280
- .filter(Boolean);
281
- }
282
-
283
- // Extract routes array (array of objects) — count + simplified entries
284
- const routesBlock = extractArrayBlock(source, 'routes');
285
- if (routesBlock) {
286
- const routes: { path: string; label?: string; group?: string; icon?: string; showInNav?: boolean }[] = [];
287
- const routeRegex = /\{\s*path:\s*['"]([^'"]+)['"][^}]*\}/g;
288
- let rm;
289
- while ((rm = routeRegex.exec(routesBlock)) !== null) {
290
- const entry: { path: string; label?: string; group?: string; icon?: string } = { path: rm[1] };
291
- const labelMatch = rm[0].match(/label:\s*['"]([^'"]+)['"]/);
292
- if (labelMatch) entry.label = labelMatch[1];
293
- const groupMatch = rm[0].match(/group:\s*['"]([^'"]+)['"]/);
294
- if (groupMatch) entry.group = groupMatch[1];
295
- const iconMatch = rm[0].match(/icon:\s*['"]([^'"]+)['"]/);
296
- if (iconMatch) entry.icon = iconMatch[1];
297
- routes.push(entry);
298
- }
299
- if (routes.length > 0) manifest.routes = routes;
300
- }
301
-
302
- // Extract actions array (array of objects)
303
- const actionsBlock = extractArrayBlock(source, 'actions');
304
- if (actionsBlock) {
305
- const actions: { id: string; description?: string }[] = [];
306
- const actionRegex = /\{\s*id:\s*['"]([^'"]+)['"][^}]*\}/g;
307
- let am;
308
- while ((am = actionRegex.exec(actionsBlock)) !== null) {
309
- const entry: { id: string; description?: string } = { id: am[1] };
310
- const descMatch = am[0].match(/description:\s*['"]([^'"]+)['"]/);
311
- if (descMatch) entry.description = descMatch[1];
312
- actions.push(entry);
313
- }
314
- if (actions.length > 0) manifest.actions = actions;
315
- }
316
-
317
- // Extract contributions array
318
- const contribsBlock = extractArrayBlock(source, 'contributions');
319
- if (contribsBlock) {
320
- const contributions: { slot: string; view: string; priority?: number }[] = [];
321
- const contribRegex = /\{\s*slot:\s*['"]([^'"]+)['"][^}]*\}/g;
322
- let cm;
323
- while ((cm = contribRegex.exec(contribsBlock)) !== null) {
324
- const entry: { slot: string; view: string; priority?: number } = { slot: cm[1], view: '' };
325
- const viewMatch = cm[0].match(/view:\s*['"]([^'"]+)['"]/);
326
- if (viewMatch) entry.view = viewMatch[1];
327
- const prioMatch = cm[0].match(/priority:\s*(\d+)/);
328
- if (prioMatch) entry.priority = parseInt(prioMatch[1], 10);
329
- contributions.push(entry);
330
- }
331
- if (contributions.length > 0) manifest.contributions = contributions;
332
- }
333
-
334
- // Extract dependencies array
335
- const depsBlock = extractArrayBlock(source, 'dependencies');
336
- if (depsBlock) {
337
- const dependencies: { slug: string; version?: string; required?: boolean; routeConfig?: { prefix?: string }; slotMapping?: Record<string, string> }[] = [];
338
- const depRegex = /\{\s*slug:\s*['"]([^'"]+)['"][^}]*\}/g;
339
- let dm;
340
- while ((dm = depRegex.exec(depsBlock)) !== null) {
341
- const entry: typeof dependencies[0] = { slug: dm[1] };
342
- const verMatch = dm[0].match(/version:\s*['"]([^'"]+)['"]/);
343
- if (verMatch) entry.version = verMatch[1];
344
- const reqMatch = dm[0].match(/required:\s*(true|false)/);
345
- if (reqMatch) entry.required = reqMatch[1] === 'true';
346
- // Extract routeConfig.prefix
347
- const prefixMatch = dm[0].match(/prefix:\s*['"]([^'"]+)['"]/);
348
- if (prefixMatch) entry.routeConfig = { prefix: prefixMatch[1] };
349
- dependencies.push(entry);
350
- }
351
- if (dependencies.length > 0) manifest.dependencies = dependencies;
352
- }
353
-
354
- // Only return a manifest if it has rich fields beyond basic metadata.
355
- // Plain blueprints with just slug/name/version don't need a manifest entry.
356
- const hasRichFields = manifest.routes || manifest.actions
357
- || manifest.contributions
358
- || manifest.capabilities || manifest.dependencies;
359
- return hasRichFields ? manifest : null;
360
- }
361
-
362
- /**
363
- * Parses dependency routeConfig entries from mm.config.ts in the file map.
364
- * Returns a map of dependency slug → { prefix, routes } for route merging.
365
- */
366
- function parseDependencyRouteConfigs(
367
- files: Record<string, string>,
368
- ): Map<string, { prefix?: string; routes?: Record<string, string | false> }> {
369
- const result = new Map<string, { prefix?: string; routes?: Record<string, string | false> }>();
370
-
371
- let configSource: string | undefined;
372
- for (const [filename, source] of Object.entries(files)) {
373
- if (isConfigFile(filename)) {
374
- configSource = source;
375
- break;
376
- }
377
- }
378
- if (!configSource) return result;
379
-
380
- const depsBlock = extractArrayBlock(configSource, 'dependencies');
381
- if (!depsBlock) return result;
382
-
383
- // Extract each dependency object block (handling nested braces)
384
- const depObjects = extractNestedObjects(depsBlock);
385
- for (const depSrc of depObjects) {
386
- const slugMatch = depSrc.match(/slug:\s*['"]([^'"]+)['"]/);
387
- if (!slugMatch) continue;
388
- const slug = slugMatch[1];
389
-
390
- // Extract routeConfig block
391
- const rcBlock = extractObjectBlock(depSrc, 'routeConfig');
392
- if (!rcBlock) continue;
393
-
394
- const entry: { prefix?: string; routes?: Record<string, string | false> } = {};
395
-
396
- const prefixMatch = rcBlock.match(/prefix:\s*['"]([^'"]+)['"]/);
397
- if (prefixMatch) entry.prefix = prefixMatch[1];
398
-
399
- // Extract per-route overrides: routes: { '/path': '/remap' | false }
400
- const routesBlock = extractObjectBlock(rcBlock, 'routes');
401
- if (routesBlock) {
402
- const overrides: Record<string, string | false> = {};
403
- const overrideRegex = /['"]([^'"]+)['"]\s*:\s*(?:['"]([^'"]+)['"]|(false))/g;
404
- let om;
405
- while ((om = overrideRegex.exec(routesBlock)) !== null) {
406
- overrides[om[1]] = om[3] === 'false' ? false : om[2];
407
- }
408
- if (Object.keys(overrides).length > 0) entry.routes = overrides;
409
- }
410
-
411
- result.set(slug, entry);
412
- }
413
-
414
- return result;
415
- }
416
-
417
- /**
418
- * Extracts top-level object blocks from a source string (e.g., array of objects).
419
- * Handles nested braces correctly.
420
- */
421
- function extractNestedObjects(source: string): string[] {
422
- const objects: string[] = [];
423
- let depth = 0;
424
- let start = -1;
425
- for (let i = 0; i < source.length; i++) {
426
- if (source[i] === '{') {
427
- if (depth === 0) start = i;
428
- depth++;
429
- } else if (source[i] === '}') {
430
- depth--;
431
- if (depth === 0 && start >= 0) {
432
- objects.push(source.slice(start, i + 1));
433
- start = -1;
434
- }
435
- }
436
- }
437
- return objects;
438
- }
439
-
440
- /**
441
- * Extracts a named object block (e.g., routeConfig: { ... }) from source.
442
- * Handles nested braces.
443
- */
444
- function extractObjectBlock(source: string, fieldName: string): string | null {
445
- const pattern = new RegExp(`${fieldName}:\\s*\\{`);
446
- const match = pattern.exec(source);
447
- if (!match) return null;
448
-
449
- let depth = 1;
450
- const startIdx = match.index + match[0].length;
451
- for (let i = startIdx; i < source.length; i++) {
452
- if (source[i] === '{') depth++;
453
- else if (source[i] === '}') {
454
- depth--;
455
- if (depth === 0) return source.slice(startIdx, i);
456
- }
457
- }
458
- return null;
459
- }
460
-
461
- /**
462
- * Extract a top-level array block from source code by field name.
463
- * Handles nested brackets. Returns the content between the outermost [].
464
- */
465
- function extractArrayBlock(source: string, fieldName: string): string | null {
466
- const startPattern = new RegExp(`${fieldName}:\\s*\\[`);
467
- const match = startPattern.exec(source);
468
- if (!match) return null;
469
-
470
- let depth = 1;
471
- const startIdx = match.index + match[0].length;
472
- for (let i = startIdx; i < source.length; i++) {
473
- if (source[i] === '[') depth++;
474
- else if (source[i] === ']') {
475
- depth--;
476
- if (depth === 0) return source.slice(startIdx, i);
477
- }
478
- }
479
- return null;
480
- }
481
-
482
- // =============================================================================
483
- // File Classification
484
- // =============================================================================
485
-
486
- function isWorkflowFile(filename: string): boolean {
487
- return /\.workflow\.(tsx?|jsx?)$/.test(filename);
488
- }
489
-
490
- function isModelFile(filename: string): boolean {
491
- return /models\/.*\.(ts|tsx)$/.test(filename) && !filename.endsWith('.test.ts');
492
- }
493
-
494
- function isServerActionFile(filename: string): boolean {
495
- return /\.server\.(ts|tsx)$/.test(filename);
496
- }
497
-
498
- function isComponentFile(filename: string): boolean {
499
- return /components\/.*\.(tsx?|jsx?)$/.test(filename)
500
- && !filename.endsWith('.test.ts')
501
- && !filename.endsWith('.test.tsx');
502
- }
503
-
504
- function isPageFile(filename: string): boolean {
505
- return (/pages\/.*\.(tsx?|jsx?)$/.test(filename) || /app\/.*\.(tsx?|jsx?)$/.test(filename))
506
- && !filename.endsWith('.test.ts')
507
- && !filename.endsWith('.test.tsx')
508
- && !filename.includes('layout');
509
- }
510
-
511
- function isAppDirFile(filename: string): boolean {
512
- return /^app\//.test(filename);
513
- }
514
-
515
- function isConfigFile(filename: string): boolean {
516
- return /mm\.config\.(ts|tsx|js)$/.test(filename);
517
- }
518
-
519
- function isModuleManifestFile(filename: string): boolean {
520
- return /mm\.module\.(ts|tsx|js)$/.test(filename);
521
- }
522
-
523
- function isCompilableFile(filename: string): boolean {
524
- return isWorkflowFile(filename)
525
- || isModelFile(filename)
526
- || isServerActionFile(filename)
527
- || isPageFile(filename)
528
- || isComponentFile(filename);
529
- }
530
-
531
- // =============================================================================
532
- // Single-File Compilation
533
- // =============================================================================
534
-
535
- function compileFile(
536
- filename: string,
537
- source: string,
538
- mode: 'strict' | 'infer',
539
- ): { ir: IRWorkflowDefinition | null; errors: ProjectCompilationError[] } {
540
- const errors: ProjectCompilationError[] = [];
541
-
542
- try {
543
- const parserPlugins = filename.endsWith('.tsx') || filename.endsWith('.jsx')
544
- ? ['typescript', 'jsx'] as const
545
- : ['typescript'] as const;
546
-
547
- const result = transformSync(source, {
548
- filename,
549
- plugins: [[babelPlugin, { mode }]],
550
- parserOpts: { plugins: parserPlugins as any, attachComment: true },
551
- });
552
-
553
- const ir: IRWorkflowDefinition | null =
554
- (result as any)?.metadata?.mindmatrixIR ?? null;
555
-
556
- // Collect per-file errors/warnings from IR metadata
557
- if (ir?.metadata) {
558
- const meta = ir.metadata as Record<string, unknown>;
559
- const fileErrors = meta.errors as ReactCompilerError[] | undefined;
560
- const fileWarnings = meta.warnings as ReactCompilerError[] | undefined;
561
- if (fileErrors) {
562
- for (const e of fileErrors) {
563
- errors.push({
564
- file: filename,
565
- message: e.message,
566
- line: e.line,
567
- column: e.column,
568
- endLine: e.line,
569
- endColumn: e.column !== undefined ? e.column + 10 : undefined,
570
- severity: 'error',
571
- });
572
- }
573
- }
574
- if (fileWarnings) {
575
- for (const w of fileWarnings) {
576
- errors.push({
577
- file: filename,
578
- message: w.message,
579
- line: w.line,
580
- column: w.column,
581
- endLine: w.line,
582
- endColumn: w.column !== undefined ? w.column + 10 : undefined,
583
- severity: 'warning',
584
- });
585
- }
586
- }
587
- }
588
-
589
- return { ir, errors };
590
- } catch (err) {
591
- // Extract line/column from Babel parse errors
592
- const errMsg = (err as Error).message;
593
- const locMatch = errMsg.match(/\((\d+):(\d+)\)/);
594
- const line = locMatch ? parseInt(locMatch[1], 10) : undefined;
595
- const column = locMatch ? parseInt(locMatch[2], 10) : undefined;
596
-
597
- errors.push({
598
- file: filename,
599
- message: 'Compilation failed: ' + errMsg,
600
- line,
601
- column,
602
- endLine: line,
603
- endColumn: column !== undefined ? column + 1 : undefined,
604
- severity: 'error',
605
- });
606
- return { ir: null, errors };
607
- }
608
- }
609
-
610
- // =============================================================================
611
- // IR Merging (for parent blueprint)
612
- // =============================================================================
613
-
614
- /** Deduplicate state actions by JSON content equality. */
615
- function deduplicateActions<T>(actions: T[]): T[] {
616
- const seen = new Set<string>();
617
- const result: T[] = [];
618
- for (const action of actions) {
619
- const key = JSON.stringify(action);
620
- if (!seen.has(key)) {
621
- seen.add(key);
622
- result.push(action);
623
- }
624
- }
625
- return result;
626
- }
627
-
628
- /**
629
- * Merges multiple IRWorkflowDefinitions into one unified parent blueprint.
630
- */
631
- function mergeIRs(
632
- irs: IRWorkflowDefinition[],
633
- config: ParsedConfig,
634
- ): IRWorkflowDefinition {
635
- if (irs.length === 0) {
636
- return createEmptyIR(config);
637
- }
638
-
639
- if (irs.length === 1) {
640
- return applyConfig(irs[0], config);
641
- }
642
-
643
- // Merge fields: deduplicate by name, first wins but merge metadata.
644
- // Later occurrences can upgrade: required=true wins, and non-empty/non-default
645
- // default_value wins (workflow files have user-specified defaults, model files
646
- // have type-inferred defaults like "" or 0).
647
- const fieldMap = new Map<string, IRFieldDefinition>();
648
- for (const ir of irs) {
649
- for (const field of ir.fields) {
650
- if (!fieldMap.has(field.name)) {
651
- fieldMap.set(field.name, field);
652
- } else {
653
- const existing = fieldMap.get(field.name)!;
654
- if (field.required && !existing.required) {
655
- existing.required = true;
656
- }
657
- // Prefer non-trivial default_value (not empty string, 0, false, null, undefined)
658
- if (field.default_value != null && field.default_value !== '' &&
659
- field.default_value !== 0 && field.default_value !== false) {
660
- if (existing.default_value == null || existing.default_value === '' ||
661
- existing.default_value === 0 || existing.default_value === false) {
662
- existing.default_value = field.default_value;
663
- }
664
- }
665
- }
666
- }
667
- }
668
-
669
- // Merge states: deduplicate by name, merge actions (deduplicate by action id)
670
- const stateMap = new Map<string, IRStateDefinition>();
671
- for (const ir of irs) {
672
- for (const state of ir.states) {
673
- if (stateMap.has(state.name)) {
674
- const existing = stateMap.get(state.name)!;
675
- existing.on_enter = deduplicateActions([...existing.on_enter, ...state.on_enter]);
676
- existing.on_exit = deduplicateActions([...existing.on_exit, ...state.on_exit]);
677
- existing.during = deduplicateActions([...existing.during, ...state.during]);
678
- if (state.on_event) {
679
- existing.on_event = [...(existing.on_event || []), ...state.on_event];
680
- }
681
- // State type: first wins (consistent with field merging).
682
- // Model files define authoritative state types via the explicit states array.
683
- } else {
684
- stateMap.set(state.name, { ...state });
685
- }
686
- }
687
- }
688
-
689
- // Merge transitions: deduplicate by name, first wins
690
- const transitionMap = new Map<string, IRTransitionDefinition>();
691
- for (const ir of irs) {
692
- for (const transition of ir.transitions) {
693
- if (!transitionMap.has(transition.name)) {
694
- transitionMap.set(transition.name, transition);
695
- }
696
- }
697
- }
698
-
699
- // Merge events
700
- const events: IROnEventSubscription[] = [];
701
- for (const ir of irs) {
702
- if (ir.on_event) {
703
- events.push(...ir.on_event);
704
- }
705
- }
706
-
707
- // Merge views: collect all view trees
708
- const viewTrees: IRExperienceNode[] = [];
709
- for (const ir of irs) {
710
- const views = (ir as any).views as Record<string, IRExperienceNode> | undefined;
711
- if (views?.default) {
712
- viewTrees.push(views.default);
713
- }
714
- }
715
-
716
- // Merge extensions (grammar islands)
717
- const extensions: Record<string, IRGrammarIsland[]> = {};
718
- for (const ir of irs) {
719
- if (ir.extensions) {
720
- for (const [key, islands] of Object.entries(ir.extensions)) {
721
- if (!extensions[key]) extensions[key] = [];
722
- extensions[key].push(...islands);
723
- }
724
- }
725
- }
726
-
727
- // Merge metadata
728
- const metadata: Record<string, unknown> = {};
729
- for (const ir of irs) {
730
- if (ir.metadata) {
731
- for (const [key, value] of Object.entries(ir.metadata)) {
732
- if (key === 'errors' || key === 'warnings') continue;
733
- metadata[key] = value;
734
- }
735
- }
736
- }
737
-
738
- // Merge roles
739
- const roleMap = new Map<string, (typeof irs)[0]['roles'][0]>();
740
- for (const ir of irs) {
741
- for (const role of ir.roles) {
742
- if (!roleMap.has(role.name)) {
743
- roleMap.set(role.name, role);
744
- }
745
- }
746
- }
747
-
748
- const base = irs[0];
749
- const merged: IRWorkflowDefinition = {
750
- slug: base.slug,
751
- name: base.name,
752
- version: base.version,
753
- description: base.description,
754
- category: base.category,
755
- fields: Array.from(fieldMap.values()),
756
- states: Array.from(stateMap.values()),
757
- transitions: Array.from(transitionMap.values()),
758
- roles: Array.from(roleMap.values()),
759
- tags: base.tags,
760
- metadata,
761
- };
762
-
763
- if (events.length > 0) {
764
- merged.on_event = events;
765
- }
766
-
767
- if (Object.keys(extensions).length > 0) {
768
- merged.extensions = extensions;
769
- }
770
-
771
- if (viewTrees.length === 1) {
772
- (merged as any).views = { default: viewTrees[0] };
773
- } else if (viewTrees.length > 1) {
774
- const rootView: IRExperienceNode = {
775
- id: 'project-root',
776
- component: 'Stack',
777
- children: viewTrees,
778
- };
779
- (merged as any).views = { default: rootView };
780
- }
781
-
782
- return applyConfig(merged, config);
783
- }
784
-
785
- function applyConfig(ir: IRWorkflowDefinition, config: ParsedConfig): IRWorkflowDefinition {
786
- if (config.slug) ir.slug = config.slug;
787
- if (config.name) ir.name = config.name;
788
- if (config.version) ir.version = config.version;
789
- if (config.description !== undefined) ir.description = config.description;
790
- if (config.category) ir.category = config.category;
791
-
792
- if (!ir.metadata) ir.metadata = {};
793
- ir.metadata.stable_id = 'def-' + ir.slug;
794
- ir.metadata.provenance = {
795
- frontend: 'react-compiler',
796
- source: 'project',
797
- compiler_version: '2.0.0',
798
- };
799
-
800
- return ir;
801
- }
802
-
803
- function createEmptyIR(config: ParsedConfig): IRWorkflowDefinition {
804
- return {
805
- slug: config.slug || 'project',
806
- name: config.name || 'Project',
807
- version: config.version || '0.1.0',
808
- description: config.description,
809
- category: config.category || 'workflow',
810
- fields: [],
811
- states: [{
812
- name: 'draft',
813
- type: 'START',
814
- on_enter: [],
815
- during: [],
816
- on_exit: [],
817
- }],
818
- transitions: [],
819
- roles: [],
820
- tags: [],
821
- metadata: {
822
- stable_id: 'def-' + (config.slug || 'project'),
823
- provenance: {
824
- frontend: 'react-compiler',
825
- source: 'project',
826
- compiler_version: '2.0.0',
827
- },
828
- },
829
- };
830
- }
831
-
832
- // =============================================================================
833
- // Cross-file Import Resolution (Phase 2 enhanced)
834
- // =============================================================================
835
-
836
- /**
837
- * Resolves cross-file imports and classifies them by link type.
838
- *
839
- * Import classification:
840
- * - pages/index.tsx importing from models/user.ts → 'data-source' link
841
- * - pages/form.tsx importing from components/MapView.tsx → 'component' (opaque pass-through)
842
- * - workflows/ride.workflow.tsx importing from actions/pricing.server.ts → 'action' link
843
- * - other imports → 'unknown'
844
- */
845
- function resolveImportLinks(
846
- files: Record<string, string>,
847
- compilableFiles: string[],
848
- ): ImportLink[] {
849
- const links: ImportLink[] = [];
850
-
851
- for (const filename of compilableFiles) {
852
- const source = files[filename];
853
- // Match import statements with named imports
854
- const importRegex = /import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+))\s+from\s+['"](\.[^'"]+)['"]/g;
855
- let match;
856
- while ((match = importRegex.exec(source)) !== null) {
857
- const namedImports = match[1];
858
- const defaultImport = match[2];
859
- const importPath = match[3];
860
-
861
- const resolved = resolveImport(filename, importPath, Object.keys(files));
862
- if (!resolved || resolved === filename) continue;
863
-
864
- // Classify the link type
865
- let linkType: ImportLink['linkType'] = 'unknown';
866
- if (isModelFile(resolved)) {
867
- linkType = 'data-source';
868
- } else if (isServerActionFile(resolved)) {
869
- linkType = 'action';
870
- } else if (/components\//.test(resolved)) {
871
- linkType = 'component';
872
- }
873
-
874
- // Extract imported symbols
875
- const symbols: string[] = [];
876
- if (namedImports) {
877
- symbols.push(...namedImports.split(',').map(s => s.trim().split(' as ')[0].trim()).filter(Boolean));
878
- }
879
- if (defaultImport) {
880
- symbols.push(defaultImport);
881
- }
882
-
883
- links.push({
884
- fromFile: filename,
885
- toFile: resolved,
886
- linkType,
887
- symbols,
888
- });
889
- }
890
- }
891
-
892
- return links;
893
- }
894
-
895
- /**
896
- * Resolves compilation order using the incremental-compiler module.
897
- */
898
- function resolveCompilationOrder(
899
- files: Record<string, string>,
900
- compilableFiles: string[],
901
- ): string[] {
902
- const { dependencies } = buildDependencyGraph(files);
903
- return topologicalSort(compilableFiles, dependencies);
904
- }
905
-
906
- // =============================================================================
907
- // Component Props Extraction
908
- // =============================================================================
909
-
910
- /**
911
- * Extracts prop names from a component file's function parameter destructuring.
912
- *
913
- * Matches patterns like:
914
- * export function MapView({ pickupLocation, dropoffLocation, ...rest }: MapViewProps)
915
- * function MapView({ pickupLocation, dropoffLocation }: MapViewProps)
916
- *
917
- * Returns an array of prop names (without types, defaults, or rest params).
918
- */
919
- function extractComponentProps(source: string): string[] {
920
- // Match: (export )?(default )?function ComponentName({ prop1, prop2, ... }
921
- const match = source.match(/function\s+\w+\s*\(\s*\{([^}]+)\}/);
922
- if (!match) return [];
923
- return match[1]
924
- .split(',')
925
- .map(p => p.trim().split(/[\s=:]/)[0].replace(/^\.{3}/, '').trim())
926
- .filter(Boolean);
927
- }
928
-
929
- // =============================================================================
930
- // Composed Result Builder (multi-workflow composition)
931
- // =============================================================================
932
-
933
- /**
934
- * Builds a composed project result with child definitions.
935
- * Phase 2: optionally uses model-compiler, route-extractor, and action-compiler.
936
- */
937
- function buildComposedResult(
938
- files: Record<string, string>,
939
- fileIRs: Record<string, IRWorkflowDefinition>,
940
- config: ParsedConfig,
941
- errors: ProjectCompilationError[],
942
- warnings: ProjectCompilationError[],
943
- options: { usePhase2Modules?: boolean; mode?: 'strict' | 'infer'; resolvedModules?: ResolvedModule[] } = {},
944
- ): ProjectCompilationResult {
945
- const usePhase2 = options.usePhase2Modules !== false; // default true
946
-
947
- // Separate IRs by file type
948
- const workflowIRs: IRWorkflowDefinition[] = [];
949
- const modelIRs: IRWorkflowDefinition[] = [];
950
- const serverActionEntries: ServerActionEntry[] = [];
951
-
952
- // Phase 2 results
953
- let modelResults: Map<string, ModelCompilationResult> | undefined;
954
- let actionResult: ActionCompilationResult | undefined;
955
- let routeResult: RouteExtractionResult | undefined;
956
-
957
- // Component definitions captured from components/*.tsx files
958
- const componentDefinitions: Record<string, { experience: IRExperienceNode; props: string[] }> = {};
959
-
960
- for (const [filename, ir] of Object.entries(fileIRs)) {
961
- if (isWorkflowFile(filename)) {
962
- workflowIRs.push(ir);
963
- } else if (isModelFile(filename)) {
964
- if (!ir.category || ir.category === 'workflow') {
965
- ir.category = 'data';
966
- }
967
- modelIRs.push(ir);
968
- } else if (isServerActionFile(filename)) {
969
- const meta = ir.metadata as Record<string, unknown> | undefined;
970
- const actions = meta?.serverActions as ServerAction[] | undefined;
971
- if (actions) {
972
- for (const action of actions) {
973
- serverActionEntries.push({
974
- name: action.name,
975
- sourceFile: filename,
976
- async: action.async,
977
- params: action.params,
978
- description: action.description,
979
- });
980
- }
981
- }
982
- } else if (isComponentFile(filename)) {
983
- // Extract experience tree from the compiled component
984
- const views = (ir as any).views as Record<string, IRExperienceNode> | undefined;
985
- const experience = views?.default ?? (ir as any).experience as IRExperienceNode | undefined;
986
- if (experience) {
987
- // Derive component name from filename: components/MapView.tsx → MapView
988
- const baseName = filename.split('/').pop()?.replace(/\.(tsx?|jsx?)$/, '') || 'Component';
989
- // Extract props from the source via function parameter destructuring
990
- const source = files[filename];
991
- const props = source ? extractComponentProps(source) : [];
992
- componentDefinitions[baseName] = { experience, props };
993
- }
994
- }
995
- }
996
-
997
- // Phase 2: Use standalone modules for enhanced compilation
998
- if (usePhase2) {
999
- // Model compilation — re-compile models with model-compiler for richer metadata
1000
- const modelFiles: Record<string, string> = {};
1001
- for (const [filename, source] of Object.entries(files)) {
1002
- if (isModelFile(filename)) {
1003
- modelFiles[filename] = source;
1004
- }
1005
- }
1006
- if (Object.keys(modelFiles).length > 0) {
1007
- modelResults = compileModels(modelFiles, { mode: options.mode || 'infer' });
1008
- // Replace model IRs with model-compiler results (richer metadata)
1009
- modelIRs.length = 0;
1010
- for (const [, result] of modelResults) {
1011
- modelIRs.push(result.ir);
1012
- }
1013
- }
1014
-
1015
- // Action compilation — use action-compiler for endpoint registration
1016
- const actionFiles: Record<string, string> = {};
1017
- for (const [filename, source] of Object.entries(files)) {
1018
- if (isServerActionFile(filename)) {
1019
- actionFiles[filename] = source;
1020
- }
1021
- }
1022
- if (Object.keys(actionFiles).length > 0) {
1023
- actionResult = compileActions(actionFiles, {
1024
- mode: options.mode || 'infer',
1025
- blueprintSlug: config.slug || 'app',
1026
- });
1027
- // Merge action-compiler results into serverActionEntries
1028
- serverActionEntries.length = 0;
1029
- for (const reg of actionResult.actions) {
1030
- serverActionEntries.push({
1031
- name: reg.name,
1032
- sourceFile: reg.sourceFile,
1033
- async: reg.async,
1034
- params: reg.params,
1035
- description: reg.description,
1036
- body: reg.body,
1037
- returnType: reg.returnType,
1038
- });
1039
- }
1040
- }
1041
-
1042
- // Route extraction — use route-extractor for app/ directory
1043
- const appFiles: Record<string, string> = {};
1044
- for (const [filename, source] of Object.entries(files)) {
1045
- if (isAppDirFile(filename) || isPageFile(filename)) {
1046
- appFiles[filename] = source;
1047
- }
1048
- }
1049
- if (Object.keys(appFiles).length > 0) {
1050
- routeResult = extractRoutes(appFiles, {
1051
- slugPrefix: config.slug || 'app',
1052
- });
1053
- }
1054
- }
1055
-
1056
- // Build child definitions: workflow + model IRs
1057
- const childDefinitions: IRWorkflowDefinition[] = [...workflowIRs, ...modelIRs];
1058
-
1059
- // Build route table from page files (Phase 1 path or Phase 2 route-extractor)
1060
- let routeTable: RouteTableEntry[] = [];
1061
-
1062
- if (routeResult) {
1063
- // Phase 2: use route-extractor results
1064
- routeTable = routeResult.routes.map(r => ({
1065
- path: r.path,
1066
- stateName: r.stateName,
1067
- sourceFile: r.sourceFile,
1068
- params: r.params,
1069
- }));
1070
- childDefinitions.push(routeResult.routerIR);
1071
- } else {
1072
- // Phase 1 fallback: derive routes from page files
1073
- const pageFiles: PageFile[] = [];
1074
- for (const filename of Object.keys(files)) {
1075
- if (isPageFile(filename)) {
1076
- pageFiles.push({
1077
- relativePath: filename,
1078
- absolutePath: filename,
1079
- });
1080
-
1081
- const routePath = filename
1082
- .replace(/^pages\//, '')
1083
- .replace(/^app\//, '')
1084
- .replace(/\.(tsx?|jsx?)$/, '')
1085
- .replace(/\/index$/, '')
1086
- .replace(/\/page$/, '');
1087
-
1088
- const stateName = pathToStateName(routePath);
1089
- const urlPattern = pathToUrlPattern(filename, filename.split('/').pop() || 'page.tsx');
1090
- const params = extractParams(filename);
1091
-
1092
- routeTable.push({
1093
- path: urlPattern,
1094
- stateName,
1095
- sourceFile: filename,
1096
- params,
1097
- });
1098
- }
1099
- }
1100
-
1101
- if (pageFiles.length > 0) {
1102
- const routerWorkflow = extractRouterWorkflow(pageFiles, {
1103
- slug: config.slug ? config.slug + '-router' : 'app-router',
1104
- pageFileName: pageFiles[0]?.relativePath.split('/').pop() || 'page.tsx',
1105
- });
1106
- childDefinitions.push(routerWorkflow);
1107
- }
1108
- }
1109
-
1110
- // ── Route merging from resolved module dependencies ──────────────────────
1111
- if (options.resolvedModules && options.resolvedModules.length > 0) {
1112
- // Parse dependency routeConfig from mm.config.ts source
1113
- const depConfigs = parseDependencyRouteConfigs(files);
1114
-
1115
- for (const mod of options.resolvedModules) {
1116
- const depConfig = depConfigs.get(mod.slug);
1117
- const prefix = depConfig?.prefix ?? `/${mod.slug.replace(/^mod-/, '')}`;
1118
- const routeOverrides = depConfig?.routes;
1119
-
1120
- for (const modRoute of mod.routeTable) {
1121
- // Check per-route overrides
1122
- if (routeOverrides) {
1123
- const override = routeOverrides[modRoute.path];
1124
- if (override === false) continue; // disabled route
1125
- if (typeof override === 'string') {
1126
- // Remap to a different path
1127
- const existingPaths = new Set(routeTable.map(r => r.path));
1128
- if (!existingPaths.has(override)) {
1129
- routeTable.push({
1130
- path: override,
1131
- stateName: `MOD_${mod.slug.replace(/-/g, '_').toUpperCase()}_${modRoute.stateName}`,
1132
- sourceFile: modRoute.sourceFile,
1133
- params: modRoute.params,
1134
- moduleSlug: mod.slug,
1135
- });
1136
- }
1137
- continue;
1138
- }
1139
- }
1140
-
1141
- // Default: prefix the module route
1142
- const prefixedPath = prefix + (modRoute.path === '/' ? '' : modRoute.path);
1143
- const existingPaths = new Set(routeTable.map(r => r.path));
1144
- if (!existingPaths.has(prefixedPath)) {
1145
- routeTable.push({
1146
- path: prefixedPath,
1147
- stateName: `MOD_${mod.slug.replace(/-/g, '_').toUpperCase()}_${modRoute.stateName}`,
1148
- sourceFile: modRoute.sourceFile,
1149
- params: modRoute.params,
1150
- moduleSlug: mod.slug,
1151
- });
1152
- }
1153
- }
1154
- }
1155
- }
1156
-
1157
- // Merge all compilable IRs into parent blueprint
1158
- const allIRs = Object.values(fileIRs);
1159
- const parentIR = mergeIRs(allIRs, config);
1160
-
1161
- // Attach composition metadata to parent
1162
- if (!parentIR.metadata) parentIR.metadata = {};
1163
- const parentMeta = parentIR.metadata as Record<string, unknown>;
1164
- parentMeta.childSlugs = childDefinitions.map(c => c.slug);
1165
- parentMeta.serverActions = serverActionEntries;
1166
- if (routeTable.length > 0) {
1167
- parentMeta.routeTable = routeTable;
1168
- }
1169
- parentMeta.composition = {
1170
- workflowCount: workflowIRs.length,
1171
- modelCount: modelIRs.length,
1172
- serverActionCount: serverActionEntries.length,
1173
- routeCount: routeTable.length,
1174
- componentCount: Object.keys(componentDefinitions).length,
1175
- totalFiles: Object.keys(files).length,
1176
- };
1177
-
1178
- // Store component definitions in parent metadata for decompiler round-trip
1179
- if (Object.keys(componentDefinitions).length > 0) {
1180
- parentMeta.componentDefinitions = componentDefinitions;
1181
- }
1182
-
1183
- // Phase 2: add action endpoints to metadata
1184
- if (actionResult) {
1185
- parentMeta.actionEndpoints = actionResult.actions.map(a => ({
1186
- actionId: a.actionId,
1187
- endpoint: a.endpoint,
1188
- group: a.group,
1189
- }));
1190
- }
1191
-
1192
- // Extract rich manifest fields from mm.config.ts (routes, actions, slots, etc.)
1193
- // These are stored in metadata.module_manifest so the editor can display them.
1194
- // Everything is a workflow — the config file IS the manifest.
1195
- for (const [filename, source] of Object.entries(files)) {
1196
- if (isConfigFile(filename) || isModuleManifestFile(filename)) {
1197
- const manifest = parseModuleManifest(source);
1198
- if (manifest) {
1199
- parentMeta.module_manifest = manifest;
1200
- }
1201
- break;
1202
- }
1203
- }
1204
-
1205
- // Store module dependencies with composition config (routeConfig, slotMapping)
1206
- const manifest = parentMeta.module_manifest as Record<string, unknown> | undefined;
1207
- if (manifest?.dependencies) {
1208
- parentMeta.module_dependencies = manifest.dependencies;
1209
- }
1210
-
1211
- // Store slot contributions from manifest for runtime pre-population
1212
- if (manifest?.contributions) {
1213
- parentMeta.slot_contributions = manifest.contributions;
1214
- }
1215
-
1216
- // Phase 2: Resolve cross-file import links
1217
- const compilableFiles = Object.keys(files).filter(isCompilableFile);
1218
- const importLinks = resolveImportLinks(files, compilableFiles);
1219
-
1220
- // ── Extract page experiences from file IRs ──────────────────────────────────
1221
- const pageExperiences: Record<string, IRExperienceNode> = {};
1222
- for (const [filename, ir] of Object.entries(fileIRs)) {
1223
- if (isPageFile(filename)) {
1224
- const views = (ir as any).views as Record<string, IRExperienceNode> | undefined;
1225
- if (views?.default) {
1226
- pageExperiences[filename] = views.default;
1227
- }
1228
- }
1229
- }
1230
-
1231
- // ── Set experience on parent IR (composed navigation with Router/Route atoms) ──
1232
- // When we have page experiences (app/*.tsx files), ALWAYS use Router/Route
1233
- // composition instead of the merged views (which flattens all pages into one Stack).
1234
- const parentViews = (parentIR as any).views as Record<string, IRExperienceNode> | undefined;
1235
- if (Object.keys(pageExperiences).length > 0) {
1236
- // Compose a routed experience from page experiences using Router/Route atoms
1237
- const pageEntries = Object.entries(pageExperiences);
1238
-
1239
- // Build route paths from filenames
1240
- const routeNodes: IRExperienceNode[] = pageEntries.map(([pagePath, pageTree], i) => {
1241
- const routePath = '/' + pagePath
1242
- .replace(/^app\//, '')
1243
- .replace(/\.(tsx?|jsx?)$/, '')
1244
- .replace(/\/index$/, '')
1245
- .replace(/\/page$/, '');
1246
- return {
1247
- id: `route-${i}`,
1248
- component: 'Route',
1249
- config: { path: routePath },
1250
- children: [pageTree],
1251
- };
1252
- });
1253
-
1254
- // Build navigation links
1255
- const navLinks: IRExperienceNode[] = pageEntries.map(([pagePath], i) => {
1256
- const routePath = '/' + pagePath
1257
- .replace(/^app\//, '')
1258
- .replace(/\.(tsx?|jsx?)$/, '')
1259
- .replace(/\/index$/, '')
1260
- .replace(/\/page$/, '');
1261
- const segments = routePath.split('/').filter(Boolean);
1262
- const label = segments[segments.length - 1]?.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) || 'Home';
1263
- return {
1264
- id: `nav-link-${i}`,
1265
- component: 'NavLink',
1266
- config: { to: routePath, label },
1267
- };
1268
- });
1269
-
1270
- // Add index route (renders first page at /)
1271
- routeNodes.unshift({
1272
- id: 'route-index',
1273
- component: 'Route',
1274
- config: { path: '/', exact: true },
1275
- children: [pageEntries[0][1]],
1276
- });
1277
-
1278
- // ── Add module routes to the Router experience ─────────────────────────
1279
- const moduleRoutes = routeTable.filter(r => r.moduleSlug);
1280
- for (let mi = 0; mi < moduleRoutes.length; mi++) {
1281
- const mr = moduleRoutes[mi];
1282
- routeNodes.push({
1283
- id: `mod-route-${mi}`,
1284
- component: 'Route',
1285
- config: { path: mr.path },
1286
- children: [{
1287
- id: `mod-route-${mi}-placeholder`,
1288
- component: 'ModuleView',
1289
- config: { moduleSlug: mr.moduleSlug, sourceFile: mr.sourceFile, path: mr.path },
1290
- }],
1291
- });
1292
- // Add nav links for module routes
1293
- const segments = mr.path.split('/').filter(Boolean);
1294
- const label = segments[segments.length - 1]?.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) || mr.moduleSlug;
1295
- navLinks.push({
1296
- id: `mod-nav-${mi}`,
1297
- component: 'NavLink',
1298
- config: { to: mr.path, label, moduleSlug: mr.moduleSlug },
1299
- });
1300
- }
1301
-
1302
- const composedExperience: IRExperienceNode = {
1303
- id: 'blueprint-root',
1304
- component: 'Stack',
1305
- className: 'h-full flex flex-col',
1306
- children: [
1307
- // Nav bar (always visible, not inside Router so it always renders)
1308
- {
1309
- id: 'nav-bar',
1310
- component: 'Row',
1311
- config: { gap: 1 },
1312
- className: 'px-4 py-2 border-b border-border bg-background/95 backdrop-blur-sm sticky top-0 z-10 flex-wrap',
1313
- children: navLinks,
1314
- },
1315
- // Router with page routes
1316
- {
1317
- id: 'page-router',
1318
- component: 'Router',
1319
- className: 'flex-1 min-h-0 overflow-y-auto',
1320
- children: routeNodes.map(r => ({
1321
- ...r,
1322
- children: r.children?.map(child => ({
1323
- ...child,
1324
- id: child.id || r.id + '-content',
1325
- component: child.component || 'Stack',
1326
- config: { ...child.config, gap: 4, padding: 4 },
1327
- })),
1328
- })),
1329
- },
1330
- ],
1331
- };
1332
- (parentIR as any).experience = composedExperience;
1333
-
1334
- // Add blueprint_manifest to metadata for BlueprintShell route resolution
1335
- const slug = config.slug || parentIR.slug || 'blueprint';
1336
- parentMeta.blueprint_manifest = {
1337
- routes: [
1338
- { path: `${slug}/*`, node: slug, label: config.name || parentIR.name || slug },
1339
- ],
1340
- config: {
1341
- full_bleed: true,
1342
- },
1343
- };
1344
- } else if (parentViews?.default) {
1345
- // Fallback: no page files, use merged views directly
1346
- (parentIR as any).experience = parentViews.default;
1347
- }
1348
-
1349
- // ── Set experience on child definitions from their source file IRs ────────
1350
- for (const childDef of childDefinitions) {
1351
- // Find the source file IR that produced this child definition
1352
- for (const [filename, ir] of Object.entries(fileIRs)) {
1353
- if (ir.slug === childDef.slug || (isWorkflowFile(filename) && ir.slug === childDef.slug)) {
1354
- const views = (ir as any).views as Record<string, IRExperienceNode> | undefined;
1355
- if (views?.default) {
1356
- (childDef as any).experience = views.default;
1357
- }
1358
- break;
1359
- }
1360
- }
1361
- }
1362
-
1363
- return {
1364
- ir: parentIR,
1365
- childDefinitions,
1366
- fileIRs,
1367
- routeTable,
1368
- serverActions: serverActionEntries,
1369
- errors,
1370
- warnings,
1371
- pageExperiences,
1372
- componentDefinitions,
1373
- importLinks,
1374
- modelResults,
1375
- actionResult,
1376
- routeResult,
1377
- };
1378
- }
1379
-
1380
- // =============================================================================
1381
- // Main API
1382
- // =============================================================================
1383
-
1384
- /**
1385
- * Compiles a multi-file project into a composed blueprint with child definitions.
1386
- *
1387
- * @param files - Record of filename → source code
1388
- * @param options - Compilation options
1389
- * @returns Composed result with parent IR, children, routes, actions, errors
1390
- */
1391
- export function compileProject(
1392
- files: Record<string, string>,
1393
- options: ProjectCompilerOptions = {},
1394
- ): ProjectCompilationResult {
1395
- const allErrors: ProjectCompilationError[] = [];
1396
- const allWarnings: ProjectCompilationError[] = [];
1397
- const fileIRs: Record<string, IRWorkflowDefinition> = {};
1398
-
1399
- // 1. Parse mm.config.ts if present
1400
- let config: ParsedConfig = {};
1401
- for (const [filename, source] of Object.entries(files)) {
1402
- if (isConfigFile(filename)) {
1403
- config = parseConfig(source);
1404
- break;
1405
- }
1406
- }
1407
-
1408
- // Resolve mode: config > options > default
1409
- const mode = config.mode || options.mode || 'infer';
1410
-
1411
- // 2. Find compilable files
1412
- const compilableFiles = Object.keys(files).filter(isCompilableFile);
1413
-
1414
- // 3. Resolve compilation order (dependencies first)
1415
- const orderedFiles = resolveCompilationOrder(files, compilableFiles);
1416
-
1417
- // 4. Compile each file
1418
- for (const filename of orderedFiles) {
1419
- const source = files[filename];
1420
- const { ir, errors } = compileFile(filename, source, mode);
1421
-
1422
- for (const e of errors) {
1423
- if (e.severity === 'error') allErrors.push(e);
1424
- else allWarnings.push(e);
1425
- }
1426
-
1427
- if (ir) {
1428
- fileIRs[filename] = ir;
1429
- }
1430
- }
1431
-
1432
- // 5. Build composed result with child definitions
1433
- return buildComposedResult(files, fileIRs, config, allErrors, allWarnings, {
1434
- usePhase2Modules: options.usePhase2Modules,
1435
- mode,
1436
- resolvedModules: options.resolvedModules,
1437
- });
1438
- }
1439
-
1440
- // =============================================================================
1441
- // Incremental Project Compiler (Phase 2 enhanced)
1442
- // =============================================================================
1443
-
1444
- /**
1445
- * Stateful project compiler that caches per-file IR results and only
1446
- * recompiles files whose content has changed (hash-based invalidation).
1447
- *
1448
- * Phase 2 enhancements:
1449
- * - Uses IncrementalCache from incremental-compiler module
1450
- * - Dependency-aware invalidation (model change → workflow recompile)
1451
- * - Compilation statistics tracking
1452
- */
1453
- export class IncrementalProjectCompiler {
1454
- private cache: IncrementalCache<{ ir: IRWorkflowDefinition; errors: ProjectCompilationError[] }>;
1455
- private lastConfig: ParsedConfig = {};
1456
-
1457
- constructor() {
1458
- this.cache = new IncrementalCache();
1459
- }
1460
-
1461
- /**
1462
- * Compile a project incrementally — only recompiles changed files.
1463
- */
1464
- compile(
1465
- files: Record<string, string>,
1466
- options: ProjectCompilerOptions = {},
1467
- ): ProjectCompilationResult {
1468
- const startTime = Date.now();
1469
-
1470
- // 1. Build dependency graph for dependency-aware invalidation
1471
- const { dependents } = buildDependencyGraph(files);
1472
-
1473
- // 2. Detect dirty files (content changes + transitive dependents)
1474
- const dirtySet = this.cache.detectDirtyFiles(files, dependents);
1475
- const allDirty = new Set([
1476
- ...dirtySet.contentChanged,
1477
- ...dirtySet.dependencyDirty,
1478
- ...dirtySet.added,
1479
- ]);
1480
-
1481
- // 3. Remove deleted files from cache
1482
- for (const removed of dirtySet.removed) {
1483
- this.cache.delete(removed);
1484
- }
1485
-
1486
- // 4. If nothing changed and we have cached results, rebuild from cache
1487
- if (allDirty.size === 0 && this.cache.getCachedFiles().length > 0) {
1488
- this.cache.updateStats(0, this.cache.getCachedFiles().length, Date.now() - startTime);
1489
- return this.rebuildFromCache(files, options);
1490
- }
1491
-
1492
- // 5. Parse config if changed
1493
- for (const [filename, source] of Object.entries(files)) {
1494
- if (isConfigFile(filename)) {
1495
- if (allDirty.has(filename)) {
1496
- this.lastConfig = parseConfig(source);
1497
- }
1498
- break;
1499
- }
1500
- }
1501
-
1502
- // 6. Resolve mode
1503
- const mode = this.lastConfig.mode || options.mode || 'infer';
1504
-
1505
- // 7. Find compilable files and compile dirty ones
1506
- const compilableFiles = Object.keys(files).filter(isCompilableFile);
1507
- let recompiled = 0;
1508
- let cacheHits = 0;
1509
-
1510
- for (const filename of compilableFiles) {
1511
- if (allDirty.has(filename) || !this.cache.has(filename)) {
1512
- const { ir, errors } = compileFile(filename, files[filename], mode);
1513
- if (ir) {
1514
- this.cache.set(filename, files[filename], { ir, errors });
1515
- } else {
1516
- this.cache.delete(filename);
1517
- }
1518
- recompiled++;
1519
- } else {
1520
- cacheHits++;
1521
- }
1522
- }
1523
-
1524
- // 8. Remove cached IRs for files no longer compilable
1525
- for (const cached of this.cache.getCachedFiles()) {
1526
- if (!compilableFiles.includes(cached)) {
1527
- this.cache.delete(cached);
1528
- }
1529
- }
1530
-
1531
- // 9. Build composed result from all IRs
1532
- const fileIRs: Record<string, IRWorkflowDefinition> = {};
1533
- const allErrors: ProjectCompilationError[] = [];
1534
- const allWarnings: ProjectCompilationError[] = [];
1535
-
1536
- for (const cached of this.cache.getCachedFiles()) {
1537
- const entry = this.cache.get(cached);
1538
- if (entry) {
1539
- fileIRs[cached] = entry.ir;
1540
- for (const e of entry.errors) {
1541
- if (e.severity === 'error') allErrors.push(e);
1542
- else allWarnings.push(e);
1543
- }
1544
- }
1545
- }
1546
-
1547
- this.cache.updateStats(recompiled, cacheHits, Date.now() - startTime);
1548
-
1549
- return buildComposedResult(files, fileIRs, this.lastConfig, allErrors, allWarnings, {
1550
- usePhase2Modules: options.usePhase2Modules,
1551
- mode,
1552
- resolvedModules: options.resolvedModules,
1553
- });
1554
- }
1555
-
1556
- /**
1557
- * Rebuild result from cache without recompiling anything.
1558
- */
1559
- private rebuildFromCache(
1560
- files: Record<string, string>,
1561
- options: ProjectCompilerOptions,
1562
- ): ProjectCompilationResult {
1563
- let config = this.lastConfig;
1564
- if (!config.slug) {
1565
- for (const [filename, source] of Object.entries(files)) {
1566
- if (isConfigFile(filename)) {
1567
- config = parseConfig(source);
1568
- this.lastConfig = config;
1569
- break;
1570
- }
1571
- }
1572
- }
1573
-
1574
- const fileIRs: Record<string, IRWorkflowDefinition> = {};
1575
- const allErrors: ProjectCompilationError[] = [];
1576
- const allWarnings: ProjectCompilationError[] = [];
1577
-
1578
- for (const cached of this.cache.getCachedFiles()) {
1579
- const entry = this.cache.get(cached);
1580
- if (entry) {
1581
- fileIRs[cached] = entry.ir;
1582
- for (const e of entry.errors) {
1583
- if (e.severity === 'error') allErrors.push(e);
1584
- else allWarnings.push(e);
1585
- }
1586
- }
1587
- }
1588
-
1589
- const mode = this.lastConfig.mode || options.mode || 'infer';
1590
- return buildComposedResult(files, fileIRs, config, allErrors, allWarnings, {
1591
- usePhase2Modules: options.usePhase2Modules,
1592
- mode,
1593
- resolvedModules: options.resolvedModules,
1594
- });
1595
- }
1596
-
1597
- /** Invalidate a specific file's cache. */
1598
- invalidate(filename: string): void {
1599
- this.cache.delete(filename);
1600
- }
1601
-
1602
- /** Invalidate all caches. */
1603
- invalidateAll(): void {
1604
- this.cache.clear();
1605
- this.lastConfig = {};
1606
- }
1607
-
1608
- /** Check if a file would need recompilation given its current source. */
1609
- isDirty(filename: string, source: string): boolean {
1610
- hashContent(source); // Check content is hashable
1611
- const cached = this.cache.get(filename);
1612
- return !cached;
1613
- }
1614
-
1615
- /** Get list of files currently in cache. */
1616
- getCachedFiles(): string[] {
1617
- return this.cache.getCachedFiles();
1618
- }
1619
-
1620
- /** Get compilation statistics. */
1621
- getStats(): { cachedFiles: number; totalHashes: number } & IncrementalStats {
1622
- const stats = this.cache.getStats();
1623
- return {
1624
- cachedFiles: stats.totalCached,
1625
- totalHashes: stats.totalCached,
1626
- ...stats,
1627
- };
1628
- }
1629
- }