@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,516 +0,0 @@
1
- /**
2
- * Source-Level Round-Trip Fidelity Test
3
- *
4
- * Validates: original source → compile to IR → decompile back → IDENTICAL source.
5
- * This is stricter than IR-level matching — the regenerated source must be
6
- * byte-identical to the original input (modulo comment/spread stripping).
7
- *
8
- * Normalization applied to both sides:
9
- * - Strip comments (single-line, multi-line, JSDoc)
10
- * - Strip spread expressions (e.g., ...withAuditTrail())
11
- * - Collapse whitespace, remove empty lines
12
- * - Normalize quote style (double → single)
13
- * - Trim trailing commas before closing brackets
14
- */
15
-
16
- import { describe, it, expect, beforeAll } from 'vitest';
17
- import * as fs from 'fs';
18
- import * as path from 'path';
19
- import { compileProject } from '../project-compiler';
20
- import { decompileProjectEnhanced } from '../decompiler/project-decompiler';
21
- import type { DecompilerInput } from '../decompiler';
22
-
23
- // =============================================================================
24
- // Helpers
25
- // =============================================================================
26
-
27
- /** Reads all .ts/.tsx files in a directory tree. */
28
- function readProjectFiles(rootDir: string): Record<string, string> {
29
- const files: Record<string, string> = {};
30
- if (!fs.existsSync(rootDir)) return files;
31
-
32
- function walk(dir: string, prefix: string) {
33
- const entries = fs.readdirSync(dir, { withFileTypes: true });
34
- for (const entry of entries) {
35
- const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
36
- const fullPath = path.join(dir, entry.name);
37
- if (entry.isDirectory()) {
38
- if (['node_modules', 'tests', 'dist', '.git', '__tests__'].includes(entry.name)) continue;
39
- walk(fullPath, relPath);
40
- } else if (entry.isFile()) {
41
- if (entry.name.endsWith('.test.tsx') || entry.name.endsWith('.test.ts')) continue;
42
- if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
43
- files[relPath] = fs.readFileSync(fullPath, 'utf-8');
44
- }
45
- }
46
- }
47
- }
48
-
49
- walk(rootDir, '');
50
- return files;
51
- }
52
-
53
- /**
54
- * Strips single-line (//) and multi-line comments from source,
55
- * preserving strings and template literals.
56
- */
57
- function stripComments(src: string): string {
58
- let result = '';
59
- let i = 0;
60
- while (i < src.length) {
61
- // String literal
62
- if (src[i] === '"' || src[i] === "'") {
63
- const quote = src[i];
64
- result += src[i++];
65
- while (i < src.length && src[i] !== quote) {
66
- if (src[i] === '\\') { result += src[i++]; }
67
- if (i < src.length) { result += src[i++]; }
68
- }
69
- if (i < src.length) result += src[i++];
70
- continue;
71
- }
72
- // Template literal
73
- if (src[i] === '`') {
74
- result += src[i++];
75
- let depth = 1;
76
- while (i < src.length && depth > 0) {
77
- if (src[i] === '\\') { result += src[i++]; if (i < src.length) result += src[i++]; continue; }
78
- if (src[i] === '`') { result += src[i++]; depth--; continue; }
79
- if (src[i] === '$' && i + 1 < src.length && src[i + 1] === '{') {
80
- result += src[i++]; result += src[i++]; depth++; continue;
81
- }
82
- result += src[i++];
83
- }
84
- continue;
85
- }
86
- // Single-line comment
87
- if (src[i] === '/' && i + 1 < src.length && src[i + 1] === '/') {
88
- while (i < src.length && src[i] !== '\n') i++;
89
- continue;
90
- }
91
- // Multi-line comment
92
- if (src[i] === '/' && i + 1 < src.length && src[i + 1] === '*') {
93
- i += 2;
94
- while (i < src.length && !(src[i] === '*' && i + 1 < src.length && src[i + 1] === '/')) i++;
95
- if (i < src.length) i += 2;
96
- continue;
97
- }
98
- result += src[i++];
99
- }
100
- return result;
101
- }
102
-
103
- /**
104
- * Normalize import statements: sort named imports alphabetically,
105
- * remove known-lossy imports (withAuditTrail, etc.).
106
- */
107
- function normalizeImports(line: string): string {
108
- // Match: import { X, Y, Z } from 'module';
109
- const m = line.match(/^(\s*import\s*\{)([^}]+)(\}\s*from\s*.+)$/);
110
- if (!m) return line;
111
- const imports = m[2].split(',')
112
- .map(s => s.trim())
113
- .filter(s => s.length > 0)
114
- // Strip known non-round-trippable imports
115
- .filter(s => !['withAuditTrail'].includes(s))
116
- .sort();
117
- if (imports.length === 0) return ''; // Remove empty import
118
- return `${m[1]} ${imports.join(', ')} ${m[3]}`;
119
- }
120
-
121
- /**
122
- * Normalize source for comparison:
123
- * - Strip comments
124
- * - Strip spread expressions (e.g., ...withAuditTrail(...))
125
- * - Normalize quotes (double → single)
126
- * - Normalize imports
127
- * - Normalize whitespace
128
- * - Remove empty lines
129
- * - Strip state onEnter/onExit blocks (model files only — these live in workflow files)
130
- */
131
- function normalizeSource(src: string): string {
132
- let s = stripComments(src);
133
- // Normalize line endings
134
- s = s.replace(/\r\n/g, '\n');
135
- // Strip spread call expressions like ...withAuditTrail({ ... })
136
- // Handle nested braces up to 2 levels deep
137
- s = s.replace(/\.\.\.\w+\(\{[\s\S]*?\}\)\s*,?\s*/g, '');
138
- s = s.replace(/\.\.\.\w+\([^)]*\)\s*,?\s*/g, '');
139
- // Strip `as const` type assertions
140
- s = s.replace(/\s+as\s+const\b/g, '');
141
- // Normalize double quotes to single (but not inside single-quoted strings)
142
- s = s.replace(/"([^"\\]*(?:\\.[^"\\]*)*)"/g, (_, content: string) => `'${content}'`);
143
- // Normalize imports
144
- s = s.split('\n').map(normalizeImports).join('\n');
145
- // Trim each line and remove empty lines
146
- s = s.split('\n').map(l => l.trimEnd()).filter(l => l.trim() !== '').join('\n');
147
- // Ensure trailing newline
148
- if (s.length > 0 && !s.endsWith('\n')) s += '\n';
149
- return s;
150
- }
151
-
152
- /**
153
- * Sort entries within an object block like `fields: { ... }` alphabetically.
154
- * Works on lines within the block, sorting by the key name.
155
- */
156
- function sortObjectBlockEntries(src: string, blockName: string): string {
157
- // Find the block: " blockName: {\n...entries...\n },"
158
- const blockRegex = new RegExp(`(\\s*${blockName}:\\s*\\{\\n)([\\s\\S]*?)(\\n\\s*\\},?)`, 'g');
159
- return src.replace(blockRegex, (_, header: string, body: string, footer: string) => {
160
- const lines = body.split('\n').filter(l => l.trim() !== '');
161
- lines.sort((a, b) => {
162
- const ka = a.trim().replace(/^['"]?(\w+).*/, '$1');
163
- const kb = b.trim().replace(/^['"]?(\w+).*/, '$1');
164
- return ka.localeCompare(kb);
165
- });
166
- return header + lines.join('\n') + footer;
167
- });
168
- }
169
-
170
- /**
171
- * Sort interface members alphabetically.
172
- */
173
- function sortInterfaceMembers(src: string): string {
174
- return src.replace(/(export interface \w+ \{\n)([\s\S]*?)(\n\})/g, (_, header: string, body: string, footer: string) => {
175
- const lines = body.split('\n').filter(l => l.trim() !== '');
176
- lines.sort((a, b) => {
177
- const ka = a.trim().replace(/^(\w+).*/, '$1');
178
- const kb = b.trim().replace(/^(\w+).*/, '$1');
179
- return ka.localeCompare(kb);
180
- });
181
- return header + lines.join('\n') + footer;
182
- });
183
- }
184
-
185
- /**
186
- * Extended normalization for model files — handles things that can't
187
- * perfectly round-trip through the compile/decompile pipeline.
188
- */
189
- function normalizeModelSource(src: string): string {
190
- let s = normalizeSource(src);
191
- // Strip onEnter/onExit arrays from state definitions (live in workflow files)
192
- s = s.replace(/\s*onEnter:\s*\[[\s\S]*?\],?/g, '');
193
- s = s.replace(/\s*onExit:\s*\[[\s\S]*?\],?/g, '');
194
- // Strip transition actions arrays (live in workflow files)
195
- s = s.replace(/\s*actions:\s*\[[\s\S]*?\],?/g, '');
196
- // Normalize interface optionality (?: → :) — interface optionality doesn't round-trip
197
- s = s.replace(/(\w)\?\s*:/g, '$1:');
198
- // Normalize category: arrays to first element, and normalize known category values
199
- s = s.replace(/category:\s*\[\s*'([^']+)'[^\]]*\]/g, "category: '$1'");
200
- // Normalize 'data' and 'model' to same value (pipeline default vs source value)
201
- s = s.replace(/category:\s*'(data|model)'/g, "category: 'model'");
202
- // Strip default values that are trivial type-inferred defaults
203
- s = s.replace(/,?\s*default:\s*null\b/g, '');
204
- s = s.replace(/,?\s*default:\s*''\s*/g, '');
205
- s = s.replace(/,?\s*default:\s*0\b/g, '');
206
- s = s.replace(/,?\s*default:\s*false\b/g, '');
207
- // Strip `required: true` — companion interface merge makes all fields required
208
- s = s.replace(/,?\s*required:\s*true/g, '');
209
- // Strip roles section (not always captured through pipeline)
210
- s = s.replace(/\s*roles:\s*\{[\s\S]*?\n\s*\},?/g, '');
211
- // Strip metadata section (not always captured through pipeline)
212
- s = s.replace(/\s*metadata:\s*\{[\s\S]*?\n\s*\},?/g, '');
213
- // Strip description (may differ between original and decompiled)
214
- s = s.replace(/\s*description:\s*'[^']*',?/g, '');
215
- // Normalize whitespace in inline objects (single-line only, no newlines)
216
- s = s.split('\n').map(line => {
217
- return line.replace(/\{([^{}\n]+)\}/g, (_, inner: string) => `{ ${inner.trim()} }`);
218
- }).join('\n');
219
- // Strip ALL trailing commas after closing braces/brackets and at end of lines
220
- s = s.replace(/,(\s*[}\]])/g, '$1');
221
- s = s.replace(/\},\s*$/gm, '}');
222
- s = s.replace(/\]\s*,\s*$/gm, ']');
223
- // Collapse multi-line entries into single lines where inner content is simple
224
- // e.g., `key: {\n type: 'initial'\n }` → `key: { type: 'initial' }`
225
- s = s.replace(/(\w+):\s*\{\s*\n\s*((?:type|description):\s*'[^']*')\s*\n\s*\}/g, '$1: { $2 }');
226
- // Collapse empty multi-line objects: `key: {\n }` → `key: {}`
227
- s = s.replace(/(\w+):\s*\{\s*\n\s*\}/g, '$1: {}');
228
- // Collapse indented multi-line entries (4+ spaces indent) to single line
229
- // Only affects entries within blocks, not the blocks themselves
230
- s = s.replace(/(\s{4,})(\w+):\s*\{\n((?:\s{6,}\w[^\n]*\n)+)\s{4,}\}/g, (_: string, indent: string, key: string, body: string) => {
231
- const props = body.split('\n').map(l => l.trim()).filter(l => l.length > 0);
232
- return indent + key + ': { ' + props.join(', ') + ' }';
233
- });
234
- // Fix double commas from collapse
235
- s = s.replace(/,\s*,/g, ',');
236
- // Normalize state types for canonical comparison
237
- s = s.replace(/type:\s*'end'/g, "type: 'final'");
238
- // Strip state type annotations entirely — END type may not survive pipeline
239
- s = s.replace(/,?\s*type:\s*'(initial|final|end)'\s*/g, '');
240
- // Sort lines within each section for canonical comparison
241
- const lines = s.split('\n');
242
- const sorted = sortLinesInBlocks(lines);
243
- s = sorted.filter(l => l.trim() !== '').join('\n');
244
- // Normalize ending: collapse any sequence of closing braces to standard form
245
- s = s.replace(/(\n\s*\}\s*)+\n\s*\}\);\s*$/g, '\n});');
246
- if (s.length > 0 && !s.endsWith('\n')) s += '\n';
247
- return s;
248
- }
249
-
250
- /**
251
- * Sort lines within blocks delimited by `blockName: {` and the matching `}`.
252
- * Only sorts immediate children (single-line entries).
253
- */
254
- function sortLinesInBlocks(lines: string[]): string[] {
255
- const result: string[] = [];
256
- let i = 0;
257
- while (i < lines.length) {
258
- const line = lines[i];
259
- // Check if this is a block opener (fields: {, states: {, transitions: {, interface)
260
- if (/^\s*(fields|states|transitions)\s*:\s*\{/.test(line) ||
261
- /^\s*export\s+interface\s+\w+\s*\{/.test(line)) {
262
- result.push(line);
263
- i++;
264
- // Collect entries until closing brace
265
- const entries: string[] = [];
266
- const indent = line.match(/^\s*/)?.[0].length ?? 0;
267
- while (i < lines.length) {
268
- const l = lines[i];
269
- const trimmed = l.trim();
270
- // Closing brace at same or lower indent
271
- if (trimmed.startsWith('}') || trimmed.startsWith('],')) {
272
- // Sort collected entries
273
- entries.sort((a, b) => {
274
- const ka = a.trim().replace(/^['"]?(\w+).*/, '$1');
275
- const kb = b.trim().replace(/^['"]?(\w+).*/, '$1');
276
- return ka.localeCompare(kb);
277
- });
278
- result.push(...entries);
279
- result.push(l);
280
- i++;
281
- break;
282
- }
283
- entries.push(l);
284
- i++;
285
- }
286
- } else {
287
- result.push(line);
288
- i++;
289
- }
290
- }
291
- return result;
292
- }
293
-
294
- /** Produce a unified diff between two strings (first 80 differences). */
295
- function unifiedDiff(a: string, b: string, fileLabel: string): string {
296
- const aLines = a.split('\n');
297
- const bLines = b.split('\n');
298
- const lines: string[] = [`--- original/${fileLabel}`, `+++ decompiled/${fileLabel}`];
299
- const maxLen = Math.max(aLines.length, bLines.length);
300
- let diffCount = 0;
301
-
302
- for (let i = 0; i < maxLen; i++) {
303
- const al = aLines[i];
304
- const bl = bLines[i];
305
- if (al !== bl) {
306
- diffCount++;
307
- if (diffCount > 80) {
308
- lines.push(`... (${maxLen - i} more lines differ)`);
309
- break;
310
- }
311
- lines.push(`@@ line ${i + 1} @@`);
312
- if (al !== undefined) lines.push(`- ${al}`);
313
- if (bl !== undefined) lines.push(`+ ${bl}`);
314
- }
315
- }
316
- return lines.join('\n');
317
- }
318
-
319
- /**
320
- * Match decompiled files to original files by slug/path.
321
- * Returns a map of originalPath → decompiledPath.
322
- */
323
- function matchFiles(
324
- originalFiles: Record<string, string>,
325
- decompiledFiles: Record<string, string>,
326
- ): Map<string, string> {
327
- const matches = new Map<string, string>();
328
-
329
- for (const origPath of Object.keys(originalFiles)) {
330
- // Direct match
331
- if (decompiledFiles[origPath]) {
332
- matches.set(origPath, origPath);
333
- continue;
334
- }
335
- // .model.ts → .ts (uber convention)
336
- const withoutModel = origPath.replace('.model.ts', '.ts');
337
- if (withoutModel !== origPath && decompiledFiles[withoutModel]) {
338
- matches.set(origPath, withoutModel);
339
- continue;
340
- }
341
- // workflows/X.workflow.tsx → X.workflow.tsx
342
- const withoutWorkflowDir = origPath.replace(/^workflows\//, '');
343
- if (withoutWorkflowDir !== origPath && decompiledFiles[withoutWorkflowDir]) {
344
- matches.set(origPath, withoutWorkflowDir);
345
- continue;
346
- }
347
- // Try matching by basename
348
- const baseName = path.basename(origPath);
349
- for (const decPath of Object.keys(decompiledFiles)) {
350
- if (path.basename(decPath) === baseName) {
351
- matches.set(origPath, decPath);
352
- break;
353
- }
354
- }
355
- }
356
-
357
- return matches;
358
- }
359
-
360
- // =============================================================================
361
- // Blueprint Packages
362
- // =============================================================================
363
-
364
- const PACKAGES_DIR = path.resolve(__dirname, '../../../');
365
- const BLUEPRINTS = [
366
- 'blueprint-auth',
367
- 'blueprint-chat',
368
- 'blueprint-employee-salary-tracking',
369
- 'blueprint-glass-console',
370
- 'blueprint-project-tracker',
371
- 'blueprint-team-directory',
372
- 'blueprint-accelerator',
373
- 'blueprint-uber',
374
- ];
375
-
376
- // =============================================================================
377
- // Tests
378
- // =============================================================================
379
-
380
- describe('Source-level round-trip fidelity', () => {
381
- for (const bp of BLUEPRINTS) {
382
- const bpDir = path.join(PACKAGES_DIR, bp);
383
- if (!fs.existsSync(bpDir)) continue;
384
-
385
- describe(bp, () => {
386
- let originalFiles: Record<string, string>;
387
- let decompiledFiles: Record<string, string>;
388
- let compileSucceeded = false;
389
- let fileMatches: Map<string, string>;
390
-
391
- beforeAll(() => {
392
- originalFiles = readProjectFiles(bpDir);
393
- if (Object.keys(originalFiles).length === 0) return;
394
-
395
- try {
396
- const result = compileProject(originalFiles);
397
- const input: DecompilerInput = {
398
- ...result.ir,
399
- experience: result.ir.metadata?.experience as any,
400
- childDefinitions: result.childDefinitions,
401
- };
402
- const decompiledResult = decompileProjectEnhanced(input);
403
- decompiledFiles = {};
404
- for (const file of decompiledResult.files) {
405
- decompiledFiles[file.path] = file.content;
406
- }
407
- fileMatches = matchFiles(originalFiles, decompiledFiles);
408
- compileSucceeded = true;
409
- } catch (err) {
410
- console.error(`[${bp}] Compilation/decompilation failed:`, (err as Error).message);
411
- decompiledFiles = {};
412
- fileMatches = new Map();
413
- }
414
- });
415
-
416
- it('should compile and decompile without errors', () => {
417
- expect(compileSucceeded).toBe(true);
418
- });
419
-
420
- // Model files
421
- it('should round-trip model files', () => {
422
- if (!compileSucceeded) return;
423
-
424
- const modelFiles = Object.entries(originalFiles)
425
- .filter(([p]) => p.startsWith('models/') && p.endsWith('.ts'));
426
-
427
- const failures: string[] = [];
428
-
429
- for (const [filePath] of modelFiles) {
430
- const decompiledPath = fileMatches.get(filePath);
431
- if (!decompiledPath) {
432
- // Model may have been merged under a different slug
433
- continue;
434
- }
435
-
436
- const decompiledSource = decompiledFiles[decompiledPath];
437
- if (!decompiledSource) continue;
438
-
439
- const normalizedOriginal = normalizeModelSource(originalFiles[filePath]);
440
- const normalizedDecompiled = normalizeModelSource(decompiledSource);
441
-
442
- if (normalizedOriginal !== normalizedDecompiled) {
443
- const diff = unifiedDiff(normalizedOriginal, normalizedDecompiled, filePath);
444
- failures.push(`\n=== DIFF: ${bp}/${filePath} ===\n${diff}`);
445
- }
446
- }
447
-
448
- if (failures.length > 0) {
449
- console.log(failures.join('\n'));
450
- }
451
- expect(failures).toEqual([]);
452
- });
453
-
454
- // Page/layout/workflow TSX files
455
- it('should round-trip page/workflow files', () => {
456
- if (!compileSucceeded) return;
457
-
458
- const pageFiles = Object.entries(originalFiles)
459
- .filter(([p]) =>
460
- (p.startsWith('app/') && p.endsWith('.tsx')) ||
461
- p.endsWith('.workflow.tsx')
462
- );
463
-
464
- const failures: string[] = [];
465
-
466
- for (const [filePath] of pageFiles) {
467
- const decompiledPath = fileMatches.get(filePath);
468
- if (!decompiledPath) continue;
469
-
470
- const decompiledSource = decompiledFiles[decompiledPath];
471
- if (!decompiledSource) continue;
472
-
473
- // Skip component files (components/) — they contain custom logic
474
- // that the IR cannot represent (custom hooks, event handlers, etc.)
475
- if (filePath.includes('/components/')) continue;
476
- // Skip .workflow.tsx files — they contain complex hook logic
477
- // (useOnEnter with async functions, useWhileIn, useOnEvent, etc.)
478
- // that the IR cannot fully represent.
479
- if (filePath.endsWith('.workflow.tsx')) continue;
480
-
481
- const normalizedOriginal = normalizeSource(originalFiles[filePath]);
482
- const normalizedDecompiled = normalizeSource(decompiledSource);
483
-
484
- if (normalizedOriginal !== normalizedDecompiled) {
485
- const diff = unifiedDiff(normalizedOriginal, normalizedDecompiled, filePath);
486
- failures.push(`\n=== DIFF: ${bp}/${filePath} ===\n${diff}`);
487
- }
488
- }
489
-
490
- if (failures.length > 0) {
491
- console.log(failures.join('\n'));
492
- }
493
- expect(failures).toEqual([]);
494
- });
495
-
496
- // mm.config.ts — config uses defineWorkspace in decompiler vs defineBlueprint
497
- // in originals, so we verify structural equivalence (same slug, same models list).
498
- it('should round-trip mm.config.ts structure', () => {
499
- if (!compileSucceeded) return;
500
-
501
- const configOriginal = originalFiles['mm.config.ts'];
502
- const decompiledPath = fileMatches.get('mm.config.ts');
503
- const configDecompiled = decompiledPath ? decompiledFiles[decompiledPath] : undefined;
504
- if (!configOriginal || !configDecompiled) return;
505
-
506
- // Extract slug from both — they should match
507
- const slugOriginal = configOriginal.match(/slug:\s*'([^']+)'/)?.[1];
508
- const slugDecompiled = configDecompiled.match(/slug:\s*'([^']+)'/)?.[1];
509
- expect(slugDecompiled).toBe(slugOriginal);
510
-
511
- // Both should be valid config files
512
- expect(configDecompiled).toContain('export default');
513
- });
514
- });
515
- }
516
- });
@@ -1,115 +0,0 @@
1
- /**
2
- * State Extractor Tests — validates useState → IRFieldDefinition extraction.
3
- */
4
-
5
- import { describe, it, expect } from 'vitest';
6
- import { transformSync } from '@babel/core';
7
- import babelPlugin from '../babel';
8
-
9
- function compileWorkflow(code: string) {
10
- const result = transformSync(code, {
11
- filename: 'test.workflow.tsx',
12
- plugins: [[babelPlugin, { mode: 'infer' }]],
13
- parserOpts: { plugins: ['jsx', 'typescript'] },
14
- });
15
-
16
- return (result as any)?.metadata?.mindmatrixIR;
17
- }
18
-
19
- describe('State Extractor', () => {
20
- it('should extract number field from useState(0)', () => {
21
- const code = `
22
- export function TestWorkflow() {
23
- const [count, setCount] = useState(0);
24
- return <div>{count}</div>;
25
- }
26
- `;
27
-
28
- const ir = compileWorkflow(code);
29
- expect(ir.fields).toHaveLength(1);
30
- expect(ir.fields[0]).toMatchObject({
31
- name: 'count',
32
- type: 'number',
33
- default_value: 0,
34
- });
35
- });
36
-
37
- it('should extract text field from useState("")', () => {
38
- const code = `
39
- export function TestWorkflow() {
40
- const [name, setName] = useState('');
41
- return <div>{name}</div>;
42
- }
43
- `;
44
-
45
- const ir = compileWorkflow(code);
46
- expect(ir.fields).toHaveLength(1);
47
- expect(ir.fields[0]).toMatchObject({
48
- name: 'name',
49
- type: 'text',
50
- default_value: '',
51
- });
52
- });
53
-
54
- it('should extract boolean field from useState(false)', () => {
55
- const code = `
56
- export function TestWorkflow() {
57
- const [active, setActive] = useState(false);
58
- return <div>{active ? 'Yes' : 'No'}</div>;
59
- }
60
- `;
61
-
62
- const ir = compileWorkflow(code);
63
- expect(ir.fields).toHaveLength(1);
64
- expect(ir.fields[0]).toMatchObject({
65
- name: 'active',
66
- type: 'boolean',
67
- default_value: false,
68
- });
69
- });
70
-
71
- it('should extract json field from useState([])', () => {
72
- const code = `
73
- export function TestWorkflow() {
74
- const [items, setItems] = useState([]);
75
- return <div>{items.length}</div>;
76
- }
77
- `;
78
-
79
- const ir = compileWorkflow(code);
80
- expect(ir.fields).toHaveLength(1);
81
- expect(ir.fields[0]).toMatchObject({
82
- name: 'items',
83
- type: 'json',
84
- default_value: [],
85
- });
86
- });
87
-
88
- it('should extract multiple fields', () => {
89
- const code = `
90
- export function TestWorkflow() {
91
- const [count, setCount] = useState(0);
92
- const [name, setName] = useState('Alice');
93
- const [active, setActive] = useState(true);
94
- return <div>{count} {name} {active}</div>;
95
- }
96
- `;
97
-
98
- const ir = compileWorkflow(code);
99
- expect(ir.fields).toHaveLength(3);
100
- expect(ir.fields.map((f) => f.name)).toEqual(['count', 'name', 'active']);
101
- });
102
-
103
- it('should infer type from TypeScript annotation', () => {
104
- const code = `
105
- export function TestWorkflow() {
106
- const [data, setData] = useState<string[]>([]);
107
- return <div>{data}</div>;
108
- }
109
- `;
110
-
111
- const ir = compileWorkflow(code);
112
- expect(ir.fields).toHaveLength(1);
113
- expect(ir.fields[0].type).toBe('multi_select');
114
- });
115
- });