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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/ATOM-PIPELINE.md +144 -0
  2. package/README.md +88 -40
  3. package/dist/babel/index.js +113 -6
  4. package/dist/babel/index.mjs +2 -2
  5. package/dist/chunk-3USIFFE4.mjs +2190 -0
  6. package/dist/chunk-45YMGEVT.mjs +186 -0
  7. package/dist/chunk-4FN2AISW.mjs +148 -0
  8. package/dist/chunk-4OPI5L7G.mjs +2593 -0
  9. package/dist/chunk-4RYTKOOJ.mjs +186 -0
  10. package/dist/chunk-5RKTOVR5.mjs +244 -0
  11. package/dist/chunk-5YDMOO4X.mjs +214 -0
  12. package/dist/chunk-64ZWEMLJ.mjs +148 -0
  13. package/dist/chunk-6XP4KSWQ.mjs +2190 -0
  14. package/dist/chunk-72QWL54I.mjs +175 -0
  15. package/dist/chunk-7B4TRI7C.mjs +4835 -0
  16. package/dist/chunk-7ZKGHTNB.mjs +4952 -0
  17. package/dist/chunk-CIESM3BP.mjs +33 -0
  18. package/dist/chunk-DE3ZGQAC.mjs +148 -0
  19. package/dist/chunk-DMCY3BBG.mjs +1933 -0
  20. package/dist/chunk-DPIK3PJS.mjs +244 -0
  21. package/dist/chunk-E5IVH4RE.mjs +186 -0
  22. package/dist/chunk-E6FZNUR5.mjs +4953 -0
  23. package/dist/chunk-EJRBDQDP.mjs +2607 -0
  24. package/dist/chunk-ELO4TXJL.mjs +186 -0
  25. package/dist/chunk-FKRO52XH.mjs +3446 -0
  26. package/dist/chunk-FL4YAKU6.mjs +4941 -0
  27. package/dist/chunk-FYT47UBU.mjs +5076 -0
  28. package/dist/chunk-GCLGPOJZ.mjs +148 -0
  29. package/dist/chunk-GXB4JOP7.mjs +5072 -0
  30. package/dist/chunk-HFXOUMTD.mjs +175 -0
  31. package/dist/chunk-HWIZ47US.mjs +214 -0
  32. package/dist/chunk-IB7MNPQL.mjs +4953 -0
  33. package/dist/chunk-ICSIHQCG.mjs +148 -0
  34. package/dist/chunk-JLA5VNQ3.mjs +186 -0
  35. package/dist/chunk-JQLWFCTM.mjs +214 -0
  36. package/dist/chunk-KFJJCQAL.mjs +148 -0
  37. package/dist/chunk-KJUIIEQE.mjs +186 -0
  38. package/dist/chunk-KNWTHRVQ.mjs +175 -0
  39. package/dist/chunk-KSG4XSZF.mjs +175 -0
  40. package/dist/chunk-LF5N6DOU.mjs +175 -0
  41. package/dist/chunk-LJQCM2IM.mjs +214 -0
  42. package/dist/chunk-NW6555WJ.mjs +186 -0
  43. package/dist/chunk-OMZE6VLQ.mjs +214 -0
  44. package/dist/chunk-P4BR7WVO.mjs +2190 -0
  45. package/dist/chunk-QQHVYH2X.mjs +244 -0
  46. package/dist/chunk-S5QLWLLT.mjs +186 -0
  47. package/dist/chunk-SCWGT2FY.mjs +2190 -0
  48. package/dist/chunk-SMKJUSB3.mjs +2190 -0
  49. package/dist/chunk-VCAY2KGM.mjs +175 -0
  50. package/dist/chunk-WECAV6QB.mjs +148 -0
  51. package/dist/chunk-WMKBXUCE.mjs +3228 -0
  52. package/dist/chunk-XAJ5BKKL.mjs +4947 -0
  53. package/dist/chunk-XG2X7AEA.mjs +175 -0
  54. package/dist/chunk-XG7Z23NQ.mjs +148 -0
  55. package/dist/chunk-XWZAOCQ7.mjs +2607 -0
  56. package/dist/chunk-Y6MA7ULW.mjs +148 -0
  57. package/dist/chunk-YMS7Q7LG.mjs +214 -0
  58. package/dist/chunk-ZA37XTGA.mjs +175 -0
  59. package/dist/cli/index.js +1616 -366
  60. package/dist/cli/index.mjs +8 -8
  61. package/dist/codemod/cli.mjs +1 -1
  62. package/dist/codemod/index.mjs +1 -1
  63. package/dist/dev-server-RmGHIntF.d.mts +113 -0
  64. package/dist/dev-server-RmGHIntF.d.ts +113 -0
  65. package/dist/dev-server.d.mts +1 -1
  66. package/dist/dev-server.d.ts +1 -1
  67. package/dist/dev-server.js +982 -53
  68. package/dist/dev-server.mjs +5 -5
  69. package/dist/envelope.js +113 -6
  70. package/dist/envelope.mjs +3 -3
  71. package/dist/index.d.mts +5 -1
  72. package/dist/index.d.ts +5 -1
  73. package/dist/index.js +992 -63
  74. package/dist/index.mjs +8 -8
  75. package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
  76. package/dist/init-EHO4VQ22.mjs +369 -0
  77. package/dist/init-UC3FWPIW.mjs +367 -0
  78. package/dist/init-UNSMVKIK.mjs +366 -0
  79. package/dist/init-UNV5XIDE.mjs +367 -0
  80. package/dist/project-compiler-2P4N4DR7.mjs +10 -0
  81. package/dist/project-compiler-D2LCC27O.mjs +10 -0
  82. package/dist/project-compiler-EJ3GANJE.mjs +10 -0
  83. package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
  84. package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
  85. package/dist/project-compiler-VWNNCHGO.mjs +10 -0
  86. package/dist/project-compiler-XVAAU4C5.mjs +10 -0
  87. package/dist/project-compiler-YES5FGMD.mjs +10 -0
  88. package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
  89. package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
  90. package/dist/project-decompiler-VLPR22QF.mjs +7 -0
  91. package/dist/pull-FUS5QYZS.mjs +109 -0
  92. package/dist/pull-LD5ENLGY.mjs +109 -0
  93. package/dist/testing/index.js +113 -6
  94. package/dist/testing/index.mjs +2 -2
  95. package/dist/vite/index.js +113 -6
  96. package/dist/vite/index.mjs +3 -3
  97. package/examples/uber-app/app/admin/fleet.tsx +19 -19
  98. package/package.json +4 -3
  99. package/compile-blueprint-chat.mjs +0 -99
  100. package/compile-blueprint-glass-console.mjs +0 -98
  101. package/compile-chat-defs.mjs +0 -92
  102. package/examples/uber-app/tests/payment.test.tsx +0 -129
  103. package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
  104. package/package.json.backup +0 -86
  105. package/scripts/decompile.ts +0 -226
  106. package/scripts/seed-auth.ts +0 -267
  107. package/scripts/seed-uber.ts +0 -248
  108. package/scripts/validate-uber.ts +0 -119
  109. package/seed-blueprint-chat.mjs +0 -444
  110. package/seed-blueprint-glass-console.mjs +0 -445
  111. package/seed-compiled.mjs +0 -318
  112. package/src/RoundTripValidator.ts +0 -400
  113. package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
  114. package/src/__tests__/auth-module-compilation.test.ts +0 -247
  115. package/src/__tests__/auth-template-compilation.test.ts +0 -589
  116. package/src/__tests__/change-extractor.test.ts +0 -142
  117. package/src/__tests__/cli-pull.test.ts +0 -73
  118. package/src/__tests__/cli-test.test.ts +0 -72
  119. package/src/__tests__/component-extractor.test.ts +0 -331
  120. package/src/__tests__/context-extractor.test.ts +0 -145
  121. package/src/__tests__/decompiler.test.ts +0 -718
  122. package/src/__tests__/define-blueprint.test.ts +0 -133
  123. package/src/__tests__/definition-validator.test.ts +0 -519
  124. package/src/__tests__/during-extractor.test.ts +0 -152
  125. package/src/__tests__/effect-extractor.test.ts +0 -107
  126. package/src/__tests__/event-emission.test.ts +0 -127
  127. package/src/__tests__/examples.test.ts +0 -236
  128. package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
  129. package/src/__tests__/golden-suite.test.ts +0 -403
  130. package/src/__tests__/grammar-island-extractor.test.ts +0 -289
  131. package/src/__tests__/instance-key.test.ts +0 -82
  132. package/src/__tests__/ir-migration.test.ts +0 -255
  133. package/src/__tests__/lock-file.test.ts +0 -117
  134. package/src/__tests__/model-extractor.test.ts +0 -195
  135. package/src/__tests__/model-field-acl.test.ts +0 -237
  136. package/src/__tests__/model-hooks.test.ts +0 -130
  137. package/src/__tests__/model-ref-resolution.test.ts +0 -268
  138. package/src/__tests__/model-roundtrip.test.ts +0 -502
  139. package/src/__tests__/model-runtime.test.ts +0 -112
  140. package/src/__tests__/model-transitions.test.ts +0 -183
  141. package/src/__tests__/nrt-action-trace.test.ts +0 -391
  142. package/src/__tests__/pipeline-hardening.test.ts +0 -413
  143. package/src/__tests__/project-compiler.test.ts +0 -546
  144. package/src/__tests__/project-decompiler.test.ts +0 -343
  145. package/src/__tests__/query-compilation.test.ts +0 -145
  146. package/src/__tests__/round-trip/PLAN.md +0 -158
  147. package/src/__tests__/round-trip/README.md +0 -52
  148. package/src/__tests__/round-trip/RESULTS.md +0 -86
  149. package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
  150. package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
  151. package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
  152. package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
  153. package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
  154. package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
  155. package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
  156. package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
  157. package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
  158. package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
  159. package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
  160. package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
  161. package/src/__tests__/round-trip-ir.test.ts +0 -300
  162. package/src/__tests__/round-trip.test.ts +0 -1212
  163. package/src/__tests__/route-merging.test.ts +0 -372
  164. package/src/__tests__/router-composition.test.ts +0 -489
  165. package/src/__tests__/router-extractor.test.ts +0 -176
  166. package/src/__tests__/server-action-extractor.test.ts +0 -128
  167. package/src/__tests__/smart-type-inference.test.ts +0 -365
  168. package/src/__tests__/source-envelope.test.ts +0 -284
  169. package/src/__tests__/source-fidelity.test.ts +0 -516
  170. package/src/__tests__/state-extractor.test.ts +0 -115
  171. package/src/__tests__/strict-mode.test.ts +0 -227
  172. package/src/__tests__/transition-effect-extractor.test.ts +0 -119
  173. package/src/__tests__/transition-extractor.test.ts +0 -68
  174. package/src/__tests__/ts-to-expression.test.ts +0 -462
  175. package/src/__tests__/type-generator.test.ts +0 -201
  176. package/src/__tests__/uber-validation.test.ts +0 -502
  177. package/src/action-compiler.ts +0 -361
  178. package/src/babel/emitters/experience-transform.ts +0 -199
  179. package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
  180. package/src/babel/emitters/pure-form-emitter.ts +0 -1023
  181. package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
  182. package/src/babel/extractors/change-extractor.ts +0 -199
  183. package/src/babel/extractors/component-extractor.ts +0 -907
  184. package/src/babel/extractors/computed-extractor.ts +0 -262
  185. package/src/babel/extractors/context-extractor.ts +0 -277
  186. package/src/babel/extractors/during-extractor.ts +0 -295
  187. package/src/babel/extractors/effect-extractor.ts +0 -340
  188. package/src/babel/extractors/event-extractor.ts +0 -235
  189. package/src/babel/extractors/grammar-island-extractor.ts +0 -302
  190. package/src/babel/extractors/model-extractor.ts +0 -1018
  191. package/src/babel/extractors/router-extractor.ts +0 -303
  192. package/src/babel/extractors/server-action-extractor.ts +0 -173
  193. package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
  194. package/src/babel/extractors/server-state-extractor.ts +0 -88
  195. package/src/babel/extractors/state-extractor.ts +0 -214
  196. package/src/babel/extractors/transition-effect-extractor.ts +0 -176
  197. package/src/babel/extractors/transition-extractor.ts +0 -143
  198. package/src/babel/index.ts +0 -24
  199. package/src/babel/transpilers/ts-to-expression.ts +0 -674
  200. package/src/babel/visitor.ts +0 -807
  201. package/src/cli/auth.ts +0 -255
  202. package/src/cli/build.ts +0 -288
  203. package/src/cli/deploy.ts +0 -206
  204. package/src/cli/index.ts +0 -328
  205. package/src/cli/installer.ts +0 -261
  206. package/src/cli/lock-file.ts +0 -94
  207. package/src/cli/mmrc.ts +0 -22
  208. package/src/cli/pull.ts +0 -172
  209. package/src/cli/registry-client.ts +0 -175
  210. package/src/cli/test.ts +0 -397
  211. package/src/cli/type-generator.ts +0 -243
  212. package/src/codemod/__tests__/forward.test.ts +0 -239
  213. package/src/codemod/__tests__/reverse.test.ts +0 -145
  214. package/src/codemod/__tests__/round-trip.test.ts +0 -137
  215. package/src/codemod/annotation.ts +0 -97
  216. package/src/codemod/classify.ts +0 -197
  217. package/src/codemod/cli.ts +0 -207
  218. package/src/codemod/control-flow.ts +0 -409
  219. package/src/codemod/forward.ts +0 -244
  220. package/src/codemod/import-manager.ts +0 -171
  221. package/src/codemod/index.ts +0 -120
  222. package/src/codemod/reverse.ts +0 -197
  223. package/src/codemod/rules.ts +0 -174
  224. package/src/codemod/state-transform.ts +0 -126
  225. package/src/decompiler/ast-builder.ts +0 -538
  226. package/src/decompiler/config-generator.ts +0 -151
  227. package/src/decompiler/index.ts +0 -315
  228. package/src/decompiler/project-decompiler.ts +0 -1776
  229. package/src/decompiler/project.ts +0 -862
  230. package/src/decompiler/split-strategy.ts +0 -140
  231. package/src/decompiler/state-emitter.ts +0 -1053
  232. package/src/decompiler/sx-emitter.ts +0 -318
  233. package/src/decompiler/workspace-hydrator.ts +0 -189
  234. package/src/dev-server.ts +0 -238
  235. package/src/envelope/fs-tree.ts +0 -217
  236. package/src/envelope/source-envelope.ts +0 -264
  237. package/src/envelope.ts +0 -315
  238. package/src/incremental-compiler.ts +0 -401
  239. package/src/index.ts +0 -99
  240. package/src/model-compiler.ts +0 -277
  241. package/src/project-compiler.ts +0 -1629
  242. package/src/route-extractor.ts +0 -333
  243. package/src/testing/index.ts +0 -32
  244. package/src/testing/snapshot.ts +0 -252
  245. package/src/testing/test-utils.ts +0 -226
  246. package/src/types.ts +0 -68
  247. package/src/vite/index.ts +0 -288
  248. package/test-compile.mjs +0 -142
  249. package/tsconfig.json +0 -25
  250. package/tsup.config.ts +0 -23
  251. package/vitest.config.ts +0 -9
@@ -1,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
- }