@mmapp/react-compiler 0.1.0-alpha.1 → 0.1.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATOM-PIPELINE.md +144 -0
- package/README.md +88 -40
- package/dist/auth-3UK75242.mjs +17 -0
- package/dist/babel/index.d.mts +2 -2
- package/dist/babel/index.d.ts +2 -2
- package/dist/babel/index.js +2816 -279
- package/dist/babel/index.mjs +2 -2
- package/dist/chunk-3USIFFE4.mjs +2190 -0
- package/dist/chunk-45YMGEVT.mjs +186 -0
- package/dist/chunk-4FN2AISW.mjs +148 -0
- package/dist/chunk-4OPI5L7G.mjs +2593 -0
- package/dist/chunk-4RYTKOOJ.mjs +186 -0
- package/dist/chunk-52XHYD2V.mjs +214 -0
- package/dist/chunk-5FTDWKHH.mjs +244 -0
- package/dist/chunk-5GUFFFGL.mjs +148 -0
- package/dist/chunk-5RKTOVR5.mjs +244 -0
- package/dist/chunk-5YDMOO4X.mjs +214 -0
- package/dist/chunk-64ZWEMLJ.mjs +148 -0
- package/dist/chunk-6XP4KSWQ.mjs +2190 -0
- package/dist/chunk-72QWL54I.mjs +175 -0
- package/dist/chunk-7B4TRI7C.mjs +4835 -0
- package/dist/chunk-7JRAEFRB.mjs +7510 -0
- package/dist/chunk-7T6Q5KAA.mjs +7506 -0
- package/dist/chunk-7ZKGHTNB.mjs +4952 -0
- package/dist/chunk-ABYPKRSB.mjs +215 -0
- package/dist/chunk-BZEXUPDH.mjs +175 -0
- package/dist/chunk-CIESM3BP.mjs +33 -0
- package/dist/chunk-DE3ZGQAC.mjs +148 -0
- package/dist/chunk-DMCY3BBG.mjs +1933 -0
- package/dist/chunk-DPIK3PJS.mjs +244 -0
- package/dist/chunk-E5IVH4RE.mjs +186 -0
- package/dist/chunk-E6FZNUR5.mjs +4953 -0
- package/dist/chunk-EJRBDQDP.mjs +2607 -0
- package/dist/chunk-ELO4TXJL.mjs +186 -0
- package/dist/chunk-EO6SYNCG.mjs +175 -0
- package/dist/chunk-FKRO52XH.mjs +3446 -0
- package/dist/chunk-FL4YAKU6.mjs +4941 -0
- package/dist/chunk-FYT47UBU.mjs +5076 -0
- package/dist/chunk-GCLGPOJZ.mjs +148 -0
- package/dist/chunk-GXB4JOP7.mjs +5072 -0
- package/dist/chunk-HFXOUMTD.mjs +175 -0
- package/dist/chunk-HRYR54PT.mjs +175 -0
- package/dist/chunk-HWIZ47US.mjs +214 -0
- package/dist/chunk-IB7MNPQL.mjs +4953 -0
- package/dist/chunk-ICSIHQCG.mjs +148 -0
- package/dist/chunk-J3M4GUS7.mjs +161 -0
- package/dist/chunk-J7JUAHS4.mjs +186 -0
- package/dist/chunk-JLA5VNQ3.mjs +186 -0
- package/dist/chunk-JQLWFCTM.mjs +214 -0
- package/dist/chunk-JRGFBWTN.mjs +2918 -0
- package/dist/chunk-KFJJCQAL.mjs +148 -0
- package/dist/chunk-KJUIIEQE.mjs +186 -0
- package/dist/chunk-KNWTHRVQ.mjs +175 -0
- package/dist/chunk-KSG4XSZF.mjs +175 -0
- package/dist/chunk-LF5N6DOU.mjs +175 -0
- package/dist/chunk-LJQCM2IM.mjs +214 -0
- package/dist/chunk-NTB7OEX2.mjs +2918 -0
- package/dist/chunk-NW6555WJ.mjs +186 -0
- package/dist/chunk-O4AUS7EU.mjs +148 -0
- package/dist/chunk-OMZE6VLQ.mjs +214 -0
- package/dist/chunk-OPJKP747.mjs +7506 -0
- package/dist/chunk-P4BR7WVO.mjs +2190 -0
- package/dist/chunk-QQHVYH2X.mjs +244 -0
- package/dist/chunk-R2DD5GTY.mjs +186 -0
- package/dist/chunk-S5QLWLLT.mjs +186 -0
- package/dist/chunk-SCWGT2FY.mjs +2190 -0
- package/dist/chunk-SMKJUSB3.mjs +2190 -0
- package/dist/chunk-THFYE5ZX.mjs +244 -0
- package/dist/chunk-UDDTWG5J.mjs +734 -0
- package/dist/chunk-VCAY2KGM.mjs +175 -0
- package/dist/chunk-VLTKQDJ3.mjs +244 -0
- package/dist/chunk-WBYMW4NQ.mjs +3450 -0
- package/dist/chunk-WECAV6QB.mjs +148 -0
- package/dist/chunk-WMKBXUCE.mjs +3228 -0
- package/dist/chunk-WVYY32LD.mjs +939 -0
- package/dist/chunk-XAJ5BKKL.mjs +4947 -0
- package/dist/chunk-XDVM4YHX.mjs +3450 -0
- package/dist/chunk-XG2X7AEA.mjs +175 -0
- package/dist/chunk-XG7Z23NQ.mjs +148 -0
- package/dist/chunk-XWZAOCQ7.mjs +2607 -0
- package/dist/chunk-Y6MA7ULW.mjs +148 -0
- package/dist/chunk-YMS7Q7LG.mjs +214 -0
- package/dist/chunk-Z2G5RZ4H.mjs +186 -0
- package/dist/chunk-ZA37XTGA.mjs +175 -0
- package/dist/chunk-ZE3KCHBM.mjs +2918 -0
- package/dist/cli/index.js +14720 -7199
- package/dist/cli/index.mjs +224 -183
- package/dist/codemod/cli.js +1 -1
- package/dist/codemod/cli.mjs +2 -2
- package/dist/codemod/index.d.mts +3 -3
- package/dist/codemod/index.d.ts +3 -3
- package/dist/codemod/index.js +1 -1
- package/dist/codemod/index.mjs +2 -2
- package/dist/config-PL24KEWL.mjs +219 -0
- package/dist/deploy-YAJGW6II.mjs +9 -0
- package/dist/dev-server-CrQ041KP.d.mts +79 -0
- package/dist/dev-server-CrQ041KP.d.ts +79 -0
- package/dist/dev-server-RmGHIntF.d.mts +113 -0
- package/dist/dev-server-RmGHIntF.d.ts +113 -0
- package/dist/dev-server.d.mts +2 -2
- package/dist/dev-server.d.ts +2 -2
- package/dist/dev-server.js +6424 -1597
- package/dist/dev-server.mjs +5 -5
- package/dist/envelope-ChEkuHij.d.mts +265 -0
- package/dist/envelope-ChEkuHij.d.ts +265 -0
- package/dist/envelope.d.mts +2 -2
- package/dist/envelope.d.ts +2 -2
- package/dist/envelope.js +2814 -277
- package/dist/envelope.mjs +3 -3
- package/dist/index-CEKyyazf.d.mts +104 -0
- package/dist/index-CEKyyazf.d.ts +104 -0
- package/dist/index.d.mts +168 -9
- package/dist/index.d.ts +168 -9
- package/dist/index.js +5606 -681
- package/dist/index.mjs +217 -9
- package/dist/init-7FJENUDK.mjs +407 -0
- package/{src/cli/init.ts → dist/init-7JQMAAXS.mjs} +70 -95
- package/dist/init-DQDX3QK6.mjs +369 -0
- package/dist/init-EHO4VQ22.mjs +369 -0
- package/dist/init-UC3FWPIW.mjs +367 -0
- package/dist/init-UNSMVKIK.mjs +366 -0
- package/dist/init-UNV5XIDE.mjs +367 -0
- package/dist/project-compiler-2P4N4DR7.mjs +10 -0
- package/dist/project-compiler-D2LCC27O.mjs +10 -0
- package/dist/project-compiler-EJ3GANJE.mjs +10 -0
- package/dist/project-compiler-LOQKVRZJ.mjs +10 -0
- package/dist/project-compiler-NNK32MPG.mjs +10 -0
- package/dist/project-compiler-OP2VVGJQ.mjs +10 -0
- package/dist/project-compiler-RQ6OQKRM.mjs +10 -0
- package/dist/project-compiler-VWNNCHGO.mjs +10 -0
- package/dist/project-compiler-XVAAU4C5.mjs +10 -0
- package/dist/project-compiler-YES5FGMD.mjs +10 -0
- package/dist/project-compiler-ZB4RUYVL.mjs +10 -0
- package/dist/project-compiler-ZKMQDLGU.mjs +10 -0
- package/dist/project-decompiler-FLXCEJHS.mjs +7 -0
- package/dist/project-decompiler-U55HQUHW.mjs +7 -0
- package/dist/project-decompiler-US7GAVIC.mjs +7 -0
- package/dist/project-decompiler-VLPR22QF.mjs +7 -0
- package/dist/pull-FUS5QYZS.mjs +109 -0
- package/dist/pull-KOL2QAYQ.mjs +109 -0
- package/dist/pull-LD5ENLGY.mjs +109 -0
- package/dist/pull-P44LDRWB.mjs +109 -0
- package/dist/seed-KOGEPGOJ.mjs +154 -0
- package/dist/server-VW6UPCHO.mjs +277 -0
- package/dist/testing/index.d.mts +8 -8
- package/dist/testing/index.d.ts +8 -8
- package/dist/testing/index.js +2824 -287
- package/dist/testing/index.mjs +2 -2
- package/dist/verify-BYHUKARQ.mjs +1833 -0
- package/dist/verify-OQDEQYMS.mjs +1833 -0
- package/dist/verify-SEIXUGN4.mjs +1833 -0
- package/dist/vite/index.d.mts +1 -1
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +2817 -280
- package/dist/vite/index.mjs +3 -3
- package/examples/authentication/main.workflow.tsx +1 -1
- package/examples/authentication/mm.config.ts +1 -1
- package/examples/authentication/pages/LoginPage.tsx +2 -2
- package/examples/authentication/pages/SignupPage.tsx +2 -2
- package/examples/counter.workflow.tsx +1 -1
- package/examples/dashboard.workflow.tsx +1 -1
- package/examples/invoice-approval/actions/invoice.server.ts +1 -1
- package/examples/invoice-approval/main.workflow.tsx +1 -1
- package/examples/invoice-approval/mm.config.ts +1 -1
- package/examples/invoice-approval/pages/InvoiceDetailPage.tsx +1 -1
- package/examples/invoice-approval/pages/InvoiceFormPage.tsx +1 -1
- package/examples/invoice-approval/pages/InvoiceListPage.tsx +1 -1
- package/examples/todo-app.workflow.tsx +1 -1
- package/examples/uber-app/actions/matching.server.ts +1 -1
- package/examples/uber-app/actions/notifications.server.ts +1 -1
- package/examples/uber-app/actions/payments.server.ts +1 -1
- package/examples/uber-app/actions/pricing.server.ts +1 -1
- package/examples/uber-app/app/admin/analytics.tsx +2 -2
- package/examples/uber-app/app/admin/fleet.tsx +21 -21
- package/examples/uber-app/app/admin/surge-pricing.tsx +2 -2
- package/examples/uber-app/app/driver/dashboard.tsx +2 -2
- package/examples/uber-app/app/driver/earnings.tsx +2 -2
- package/examples/uber-app/app/driver/navigation.tsx +2 -2
- package/examples/uber-app/app/driver/ride-acceptance.tsx +2 -2
- package/examples/uber-app/app/rider/home.tsx +2 -2
- package/examples/uber-app/app/rider/payment-methods.tsx +2 -2
- package/examples/uber-app/app/rider/ride-history.tsx +2 -2
- package/examples/uber-app/app/rider/ride-tracking.tsx +2 -2
- package/examples/uber-app/components/DriverCard.tsx +1 -1
- package/examples/uber-app/components/MapView.tsx +3 -3
- package/examples/uber-app/components/RatingStars.tsx +2 -2
- package/examples/uber-app/components/RideCard.tsx +1 -1
- package/examples/uber-app/mm.config.ts +1 -1
- package/examples/uber-app/workflows/dispute-resolution.workflow.tsx +2 -2
- package/examples/uber-app/workflows/driver-onboarding.workflow.tsx +2 -2
- package/examples/uber-app/workflows/payment-processing.workflow.tsx +2 -2
- package/examples/uber-app/workflows/ride-request.workflow.tsx +2 -2
- package/package.json +10 -4
- package/compile-blueprint-chat.mjs +0 -99
- package/compile-blueprint-glass-console.mjs +0 -98
- package/compile-chat-defs.mjs +0 -92
- package/examples/uber-app/tests/payment.test.tsx +0 -129
- package/examples/uber-app/tests/ride-flow.test.tsx +0 -123
- package/package.json.backup +0 -86
- package/scripts/decompile.ts +0 -226
- package/scripts/seed-auth.ts +0 -267
- package/scripts/seed-uber.ts +0 -248
- package/scripts/validate-uber.ts +0 -119
- package/seed-blueprint-chat.mjs +0 -444
- package/seed-blueprint-glass-console.mjs +0 -445
- package/seed-compiled.mjs +0 -318
- package/src/RoundTripValidator.ts +0 -400
- package/src/__tests__/atom-rendering-coverage.test.ts +0 -680
- package/src/__tests__/auth-module-compilation.test.ts +0 -247
- package/src/__tests__/auth-template-compilation.test.ts +0 -589
- package/src/__tests__/change-extractor.test.ts +0 -142
- package/src/__tests__/cli-pull.test.ts +0 -73
- package/src/__tests__/cli-test.test.ts +0 -72
- package/src/__tests__/component-extractor.test.ts +0 -331
- package/src/__tests__/context-extractor.test.ts +0 -145
- package/src/__tests__/decompiler.test.ts +0 -718
- package/src/__tests__/define-blueprint.test.ts +0 -133
- package/src/__tests__/definition-validator.test.ts +0 -519
- package/src/__tests__/during-extractor.test.ts +0 -152
- package/src/__tests__/effect-extractor.test.ts +0 -107
- package/src/__tests__/event-emission.test.ts +0 -127
- package/src/__tests__/examples.test.ts +0 -236
- package/src/__tests__/full-blueprint-coverage.test.ts +0 -1221
- package/src/__tests__/golden-suite.test.ts +0 -403
- package/src/__tests__/grammar-island-extractor.test.ts +0 -289
- package/src/__tests__/instance-key.test.ts +0 -82
- package/src/__tests__/ir-migration.test.ts +0 -255
- package/src/__tests__/lock-file.test.ts +0 -117
- package/src/__tests__/model-extractor.test.ts +0 -195
- package/src/__tests__/model-field-acl.test.ts +0 -237
- package/src/__tests__/model-hooks.test.ts +0 -130
- package/src/__tests__/model-ref-resolution.test.ts +0 -268
- package/src/__tests__/model-roundtrip.test.ts +0 -502
- package/src/__tests__/model-runtime.test.ts +0 -112
- package/src/__tests__/model-transitions.test.ts +0 -183
- package/src/__tests__/nrt-action-trace.test.ts +0 -391
- package/src/__tests__/pipeline-hardening.test.ts +0 -413
- package/src/__tests__/project-compiler.test.ts +0 -546
- package/src/__tests__/project-decompiler.test.ts +0 -343
- package/src/__tests__/query-compilation.test.ts +0 -145
- package/src/__tests__/round-trip/PLAN.md +0 -158
- package/src/__tests__/round-trip/README.md +0 -52
- package/src/__tests__/round-trip/RESULTS.md +0 -86
- package/src/__tests__/round-trip/fixtures/data-heavy/main.workflow.tsx +0 -55
- package/src/__tests__/round-trip/fixtures/data-heavy/mm.config.ts +0 -11
- package/src/__tests__/round-trip/fixtures/data-heavy/models/contact.ts +0 -54
- package/src/__tests__/round-trip/fixtures/full-workflow/main.workflow.tsx +0 -79
- package/src/__tests__/round-trip/fixtures/full-workflow/mm.config.ts +0 -12
- package/src/__tests__/round-trip/fixtures/full-workflow/models/order.ts +0 -50
- package/src/__tests__/round-trip/fixtures/simple-crud/main.workflow.tsx +0 -25
- package/src/__tests__/round-trip/fixtures/simple-crud/mm.config.ts +0 -11
- package/src/__tests__/round-trip/fixtures/simple-crud/models/task.ts +0 -32
- package/src/__tests__/round-trip/fixtures/view-heavy/main.workflow.tsx +0 -79
- package/src/__tests__/round-trip/fixtures/view-heavy/mm.config.ts +0 -10
- package/src/__tests__/round-trip/round-trip.test.ts +0 -2598
- package/src/__tests__/round-trip-ir.test.ts +0 -300
- package/src/__tests__/round-trip.test.ts +0 -1212
- package/src/__tests__/route-merging.test.ts +0 -372
- package/src/__tests__/router-composition.test.ts +0 -489
- package/src/__tests__/router-extractor.test.ts +0 -176
- package/src/__tests__/server-action-extractor.test.ts +0 -128
- package/src/__tests__/smart-type-inference.test.ts +0 -365
- package/src/__tests__/source-envelope.test.ts +0 -284
- package/src/__tests__/source-fidelity.test.ts +0 -516
- package/src/__tests__/state-extractor.test.ts +0 -115
- package/src/__tests__/strict-mode.test.ts +0 -227
- package/src/__tests__/transition-effect-extractor.test.ts +0 -119
- package/src/__tests__/transition-extractor.test.ts +0 -68
- package/src/__tests__/ts-to-expression.test.ts +0 -462
- package/src/__tests__/type-generator.test.ts +0 -201
- package/src/__tests__/uber-validation.test.ts +0 -502
- package/src/action-compiler.ts +0 -361
- package/src/babel/emitters/experience-transform.ts +0 -199
- package/src/babel/emitters/ir-to-tsx-emitter.ts +0 -110
- package/src/babel/emitters/pure-form-emitter.ts +0 -1023
- package/src/babel/emitters/runtime-glue-emitter.ts +0 -39
- package/src/babel/extractors/change-extractor.ts +0 -199
- package/src/babel/extractors/component-extractor.ts +0 -907
- package/src/babel/extractors/computed-extractor.ts +0 -262
- package/src/babel/extractors/context-extractor.ts +0 -277
- package/src/babel/extractors/during-extractor.ts +0 -295
- package/src/babel/extractors/effect-extractor.ts +0 -340
- package/src/babel/extractors/event-extractor.ts +0 -235
- package/src/babel/extractors/grammar-island-extractor.ts +0 -302
- package/src/babel/extractors/model-extractor.ts +0 -1018
- package/src/babel/extractors/router-extractor.ts +0 -303
- package/src/babel/extractors/server-action-extractor.ts +0 -173
- package/src/babel/extractors/server-action-hook-extractor.ts +0 -72
- package/src/babel/extractors/server-state-extractor.ts +0 -88
- package/src/babel/extractors/state-extractor.ts +0 -214
- package/src/babel/extractors/transition-effect-extractor.ts +0 -176
- package/src/babel/extractors/transition-extractor.ts +0 -143
- package/src/babel/index.ts +0 -24
- package/src/babel/transpilers/ts-to-expression.ts +0 -674
- package/src/babel/visitor.ts +0 -807
- package/src/cli/auth.ts +0 -255
- package/src/cli/build.ts +0 -288
- package/src/cli/deploy.ts +0 -206
- package/src/cli/index.ts +0 -328
- package/src/cli/installer.ts +0 -261
- package/src/cli/lock-file.ts +0 -94
- package/src/cli/mmrc.ts +0 -22
- package/src/cli/pull.ts +0 -172
- package/src/cli/registry-client.ts +0 -175
- package/src/cli/test.ts +0 -397
- package/src/cli/type-generator.ts +0 -243
- package/src/codemod/__tests__/forward.test.ts +0 -239
- package/src/codemod/__tests__/reverse.test.ts +0 -145
- package/src/codemod/__tests__/round-trip.test.ts +0 -137
- package/src/codemod/annotation.ts +0 -97
- package/src/codemod/classify.ts +0 -197
- package/src/codemod/cli.ts +0 -207
- package/src/codemod/control-flow.ts +0 -409
- package/src/codemod/forward.ts +0 -244
- package/src/codemod/import-manager.ts +0 -171
- package/src/codemod/index.ts +0 -120
- package/src/codemod/reverse.ts +0 -197
- package/src/codemod/rules.ts +0 -174
- package/src/codemod/state-transform.ts +0 -126
- package/src/decompiler/ast-builder.ts +0 -538
- package/src/decompiler/config-generator.ts +0 -151
- package/src/decompiler/index.ts +0 -315
- package/src/decompiler/project-decompiler.ts +0 -1776
- package/src/decompiler/project.ts +0 -862
- package/src/decompiler/split-strategy.ts +0 -140
- package/src/decompiler/state-emitter.ts +0 -1053
- package/src/decompiler/sx-emitter.ts +0 -318
- package/src/decompiler/workspace-hydrator.ts +0 -189
- package/src/dev-server.ts +0 -238
- package/src/envelope/fs-tree.ts +0 -217
- package/src/envelope/source-envelope.ts +0 -264
- package/src/envelope.ts +0 -315
- package/src/incremental-compiler.ts +0 -401
- package/src/index.ts +0 -99
- package/src/model-compiler.ts +0 -277
- package/src/project-compiler.ts +0 -1629
- package/src/route-extractor.ts +0 -333
- package/src/testing/index.ts +0 -32
- package/src/testing/snapshot.ts +0 -252
- package/src/testing/test-utils.ts +0 -226
- package/src/types.ts +0 -68
- package/src/vite/index.ts +0 -288
- package/test-compile.mjs +0 -142
- package/tsconfig.json +0 -25
- package/tsup.config.ts +0 -23
- 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
|
-
});
|