@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
package/src/project-compiler.ts
DELETED
|
@@ -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
|
-
}
|