@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,1776 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* project-decompiler.ts — Enhanced multi-file project decompiler.
|
|
3
|
-
*
|
|
4
|
-
* Takes a full WorkflowDefinition (with states, transitions, fields,
|
|
5
|
-
* experience, roles) and produces a multi-file project structure:
|
|
6
|
-
*
|
|
7
|
-
* - States/Transitions → models/{slug}.ts (exported arrays + interface)
|
|
8
|
-
* - Experience tree → app/**\/*.tsx pages based on route structure
|
|
9
|
-
* - Roles → auth guard helper in app/layout.tsx
|
|
10
|
-
* - Server actions → actions/*.server.ts files
|
|
11
|
-
* - Config → mm.config.ts
|
|
12
|
-
*
|
|
13
|
-
* Uses the SplitStrategy to determine output complexity and the
|
|
14
|
-
* ConfigGenerator for mm.config.ts emission.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type {
|
|
18
|
-
IRExperienceNode,
|
|
19
|
-
IRFieldDefinition,
|
|
20
|
-
IRStateDefinition,
|
|
21
|
-
IRTransitionDefinition,
|
|
22
|
-
} from '@mindmatrix/player-core';
|
|
23
|
-
import { decompile } from './index';
|
|
24
|
-
import type { DecompilerInput } from './index';
|
|
25
|
-
import { determineSplitStrategy } from './split-strategy';
|
|
26
|
-
import type { SplitDecision } from './split-strategy';
|
|
27
|
-
import { extractConfigData, generateMmConfig } from './config-generator';
|
|
28
|
-
import type { FileRole } from '../envelope/fs-tree';
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// Public Types
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
/** A single file produced by project decompilation. */
|
|
35
|
-
export interface EnhancedProjectFile {
|
|
36
|
-
/** Relative path from project root. */
|
|
37
|
-
path: string;
|
|
38
|
-
/** File role per envelope fs-tree. */
|
|
39
|
-
role: FileRole;
|
|
40
|
-
/** Generated source code. */
|
|
41
|
-
content: string;
|
|
42
|
-
/** SHA-256 hash of content (for workspace hydration). */
|
|
43
|
-
hash?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Result of the enhanced project decompiler. */
|
|
47
|
-
export interface EnhancedDecompileResult {
|
|
48
|
-
files: EnhancedProjectFile[];
|
|
49
|
-
entryFile: string;
|
|
50
|
-
slug: string;
|
|
51
|
-
splitDecision: SplitDecision;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// =============================================================================
|
|
55
|
-
// Name Helpers
|
|
56
|
-
// =============================================================================
|
|
57
|
-
|
|
58
|
-
function pascalCase(slug: string): string {
|
|
59
|
-
return slug
|
|
60
|
-
.split(/[-_]/)
|
|
61
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
62
|
-
.join('');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function camelCase(str: string): string {
|
|
66
|
-
return str.replace(/[-_]([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Corrects the "all same from" compiler bug in transitions.
|
|
71
|
-
* When all transitions share the same `from` value AND every transition
|
|
72
|
-
* name is an independently-known state name (from `from` + `to` fields only,
|
|
73
|
-
* NOT from the transition names themselves), the transition name is used as
|
|
74
|
-
* the actual source state.
|
|
75
|
-
*
|
|
76
|
-
* This avoids false positives where transitions like "increment" and "reset"
|
|
77
|
-
* share `from: 'idle'` but are NOT state names.
|
|
78
|
-
*/
|
|
79
|
-
function correctTransitionFromFields(
|
|
80
|
-
transitions: IRTransitionDefinition[],
|
|
81
|
-
): IRTransitionDefinition[] {
|
|
82
|
-
if (transitions.length <= 1) return transitions;
|
|
83
|
-
|
|
84
|
-
const fromValues = new Set<string>();
|
|
85
|
-
for (const t of transitions) {
|
|
86
|
-
const arr = Array.isArray(t.from) ? t.from : [t.from as unknown as string];
|
|
87
|
-
for (const f of arr) fromValues.add(f);
|
|
88
|
-
}
|
|
89
|
-
if (fromValues.size !== 1) return transitions;
|
|
90
|
-
|
|
91
|
-
// Build state set from from/to values ONLY (not from transition names)
|
|
92
|
-
const stateNames = new Set<string>([...fromValues]);
|
|
93
|
-
for (const t of transitions) {
|
|
94
|
-
stateNames.add(t.to);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Only apply correction if EVERY transition name is already a known state
|
|
98
|
-
const allNamesAreStates = transitions.every(t => stateNames.has(t.name));
|
|
99
|
-
if (!allNamesAreStates) return transitions;
|
|
100
|
-
|
|
101
|
-
return transitions.map(t => ({ ...t, from: [t.name] }));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function fieldTypeToTS(fieldType: string, field?: IRFieldDefinition): string {
|
|
105
|
-
// Check for options → emit union type
|
|
106
|
-
const options = getFieldOptions(field);
|
|
107
|
-
if (options && options.length > 0 && (fieldType === 'select' || fieldType === 'text')) {
|
|
108
|
-
return options.map(o => `'${esc(String(o))}'`).join(' | ');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
switch (fieldType) {
|
|
112
|
-
case 'text': case 'rich_text': case 'email': case 'url':
|
|
113
|
-
case 'phone': case 'color': case 'select':
|
|
114
|
-
return 'string';
|
|
115
|
-
case 'number': case 'currency': case 'percentage':
|
|
116
|
-
case 'rating': case 'duration': case 'auto_number':
|
|
117
|
-
return 'number';
|
|
118
|
-
case 'boolean':
|
|
119
|
-
return 'boolean';
|
|
120
|
-
case 'date': case 'datetime': case 'created_at': case 'updated_at':
|
|
121
|
-
return 'Date';
|
|
122
|
-
case 'multi_select':
|
|
123
|
-
return 'string[]';
|
|
124
|
-
case 'json': case 'object':
|
|
125
|
-
return 'Record<string, unknown>';
|
|
126
|
-
case 'array':
|
|
127
|
-
return 'string[]';
|
|
128
|
-
case 'file': case 'image':
|
|
129
|
-
return 'string';
|
|
130
|
-
case 'relation': case 'lookup':
|
|
131
|
-
return 'string';
|
|
132
|
-
default:
|
|
133
|
-
return 'unknown';
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Extracts options from a field's validation or metadata.
|
|
139
|
-
* Options may be stored as:
|
|
140
|
-
* - field.validation.options (primary — DB schema)
|
|
141
|
-
* - field.metadata.options (fallback)
|
|
142
|
-
* - field.options (legacy)
|
|
143
|
-
*/
|
|
144
|
-
function getFieldOptions(field?: IRFieldDefinition): string[] | null {
|
|
145
|
-
if (!field) return null;
|
|
146
|
-
const f = field as unknown as Record<string, unknown>;
|
|
147
|
-
|
|
148
|
-
// Primary: validation.options
|
|
149
|
-
const validation = f.validation as Record<string, unknown> | undefined;
|
|
150
|
-
if (validation?.options && Array.isArray(validation.options)) {
|
|
151
|
-
return validation.options as string[];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Fallback: metadata.options
|
|
155
|
-
const meta = f.metadata as Record<string, unknown> | undefined;
|
|
156
|
-
if (meta?.options && Array.isArray(meta.options)) {
|
|
157
|
-
return meta.options as string[];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Legacy: field.options
|
|
161
|
-
if (f.options && Array.isArray(f.options)) {
|
|
162
|
-
return f.options as string[];
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// =============================================================================
|
|
169
|
-
// Model File: defineModel() + companion interface
|
|
170
|
-
// =============================================================================
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Returns a valid JS object key — quoted if the name isn't a valid identifier.
|
|
174
|
-
*/
|
|
175
|
-
function safeKey(name: string): string {
|
|
176
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${esc(name)}'`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Maps an IR field type to a defineModel field type string.
|
|
181
|
-
*/
|
|
182
|
-
function fieldTypeToModelType(fieldType: string): string {
|
|
183
|
-
switch (fieldType) {
|
|
184
|
-
case 'text': case 'rich_text': case 'select':
|
|
185
|
-
return 'string';
|
|
186
|
-
case 'email': return 'email';
|
|
187
|
-
case 'url': return 'url';
|
|
188
|
-
case 'phone': return 'phone';
|
|
189
|
-
case 'color': return 'color';
|
|
190
|
-
case 'number': case 'currency': case 'percentage':
|
|
191
|
-
case 'rating': case 'duration': case 'auto_number':
|
|
192
|
-
return 'number';
|
|
193
|
-
case 'boolean':
|
|
194
|
-
return 'boolean';
|
|
195
|
-
case 'date': case 'datetime': case 'created_at': case 'updated_at':
|
|
196
|
-
return 'date';
|
|
197
|
-
case 'multi_select':
|
|
198
|
-
return 'array';
|
|
199
|
-
case 'json': case 'object':
|
|
200
|
-
return 'json';
|
|
201
|
-
case 'array':
|
|
202
|
-
return 'array';
|
|
203
|
-
case 'file': case 'image':
|
|
204
|
-
return 'file';
|
|
205
|
-
case 'relation': case 'lookup':
|
|
206
|
-
return 'relation';
|
|
207
|
-
default:
|
|
208
|
-
return fieldType;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Serializes a default value for defineModel field declarations.
|
|
214
|
-
*/
|
|
215
|
-
function serializeDefault(value: unknown): string {
|
|
216
|
-
if (value === undefined || value === null) return 'null';
|
|
217
|
-
if (typeof value === 'string') return `'${esc(value)}'`;
|
|
218
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
219
|
-
if (Array.isArray(value)) {
|
|
220
|
-
if (value.length === 0) return '[]';
|
|
221
|
-
return `[${value.map(v => serializeDefault(v)).join(', ')}]`;
|
|
222
|
-
}
|
|
223
|
-
return JSON.stringify(value);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Generates a complete model file with:
|
|
228
|
-
* - import { defineModel } from "@mindmatrix/react"
|
|
229
|
-
* - export default defineModel({ slug, version, category, fields, states, transitions })
|
|
230
|
-
* - companion TypeScript interface for IntelliSense
|
|
231
|
-
*/
|
|
232
|
-
function generateModelFile(
|
|
233
|
-
slug: string,
|
|
234
|
-
fields: IRFieldDefinition[],
|
|
235
|
-
states: IRStateDefinition[],
|
|
236
|
-
transitions: IRTransitionDefinition[],
|
|
237
|
-
meta?: { version?: string; category?: string; description?: string },
|
|
238
|
-
): string {
|
|
239
|
-
const typeName = pascalCase(slug);
|
|
240
|
-
const interfaceName = `${typeName}Fields`;
|
|
241
|
-
const version = meta?.version || '0.1.0';
|
|
242
|
-
const category = meta?.category || 'data';
|
|
243
|
-
const lines: string[] = [];
|
|
244
|
-
|
|
245
|
-
// Import
|
|
246
|
-
lines.push(`import { defineModel } from '@mindmatrix/react';`);
|
|
247
|
-
lines.push(``);
|
|
248
|
-
|
|
249
|
-
// Companion TypeScript interface for IntelliSense
|
|
250
|
-
// NOTE: Field labels are deliberately excluded to ensure round-trip stability.
|
|
251
|
-
// Fields sorted alphabetically for deterministic round-trip output.
|
|
252
|
-
const sortedFields = [...fields].sort((a, b) => a.name.localeCompare(b.name));
|
|
253
|
-
if (sortedFields.length > 0) {
|
|
254
|
-
lines.push(`export interface ${interfaceName} {`);
|
|
255
|
-
for (const field of sortedFields) {
|
|
256
|
-
const tsType = fieldTypeToTS(field.type, field);
|
|
257
|
-
const optional = field.required ? '' : '?';
|
|
258
|
-
lines.push(` ${camelCase(field.name)}${optional}: ${tsType};`);
|
|
259
|
-
}
|
|
260
|
-
lines.push(`}`);
|
|
261
|
-
lines.push(``);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// defineModel() call
|
|
265
|
-
lines.push(`export default defineModel({`);
|
|
266
|
-
lines.push(` slug: '${esc(slug)}',`);
|
|
267
|
-
lines.push(` version: '${esc(version)}',`);
|
|
268
|
-
lines.push(` category: '${esc(category)}',`);
|
|
269
|
-
if (meta?.description) {
|
|
270
|
-
lines.push(` description: '${esc(meta.description)}',`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Fields (sorted alphabetically for determinism)
|
|
274
|
-
if (sortedFields.length > 0) {
|
|
275
|
-
lines.push(` fields: {`);
|
|
276
|
-
for (const field of sortedFields) {
|
|
277
|
-
const props: string[] = [];
|
|
278
|
-
props.push(`type: '${fieldTypeToModelType(field.type)}'`);
|
|
279
|
-
if (field.required) {
|
|
280
|
-
props.push(`required: true`);
|
|
281
|
-
}
|
|
282
|
-
// Default value — emit when present. Null is emitted explicitly to
|
|
283
|
-
// distinguish "no default" (undefined) from "default is null".
|
|
284
|
-
// Skip trivial type-inferred defaults (empty string, 0, false) to keep
|
|
285
|
-
// model files clean, but always emit null for round-trip stability.
|
|
286
|
-
if (field.default_value === null) {
|
|
287
|
-
props.push(`default: null`);
|
|
288
|
-
} else if (field.default_value !== undefined
|
|
289
|
-
&& field.default_value !== '' && field.default_value !== 0
|
|
290
|
-
&& field.default_value !== false) {
|
|
291
|
-
props.push(`default: ${serializeDefault(field.default_value)}`);
|
|
292
|
-
}
|
|
293
|
-
// Enum/options for select fields
|
|
294
|
-
const options = getFieldOptions(field);
|
|
295
|
-
if (options && options.length > 0) {
|
|
296
|
-
const enumStr = options.map(o => `'${esc(String(o))}'`).join(', ');
|
|
297
|
-
props.push(`enum: [${enumStr}]`);
|
|
298
|
-
}
|
|
299
|
-
lines.push(` ${camelCase(field.name)}: { ${props.join(', ')} },`);
|
|
300
|
-
}
|
|
301
|
-
lines.push(` },`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// States (sorted alphabetically for determinism, skip empty-named states)
|
|
305
|
-
const validStates = states.filter(s => s.name !== '');
|
|
306
|
-
if (validStates.length > 0) {
|
|
307
|
-
const sortedStates = [...validStates].sort((a, b) => a.name.localeCompare(b.name));
|
|
308
|
-
lines.push(` states: {`);
|
|
309
|
-
for (const state of sortedStates) {
|
|
310
|
-
const props: string[] = [];
|
|
311
|
-
if (state.type === 'START') {
|
|
312
|
-
props.push(`type: 'initial'`);
|
|
313
|
-
} else if (state.type === 'END') {
|
|
314
|
-
props.push(`type: 'final'`);
|
|
315
|
-
}
|
|
316
|
-
if (state.description) {
|
|
317
|
-
props.push(`description: '${esc(state.description)}'`);
|
|
318
|
-
}
|
|
319
|
-
const body = props.length > 0 ? ` ${props.join(', ')} ` : '';
|
|
320
|
-
lines.push(` ${safeKey(state.name)}: {${body}},`);
|
|
321
|
-
}
|
|
322
|
-
lines.push(` },`);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Transitions (sorted alphabetically for determinism, with from-field correction)
|
|
326
|
-
if (transitions.length > 0) {
|
|
327
|
-
const correctedTrans = correctTransitionFromFields(transitions);
|
|
328
|
-
const sortedTrans = [...correctedTrans].sort((a, b) => a.name.localeCompare(b.name));
|
|
329
|
-
lines.push(` transitions: {`);
|
|
330
|
-
for (const trans of sortedTrans) {
|
|
331
|
-
const fromArr = Array.isArray(trans.from) ? trans.from : [trans.from as unknown as string];
|
|
332
|
-
const parts: string[] = [];
|
|
333
|
-
if (fromArr.length === 1) {
|
|
334
|
-
parts.push(`from: '${esc(fromArr[0])}'`);
|
|
335
|
-
} else {
|
|
336
|
-
parts.push(`from: [${fromArr.map(f => `'${esc(f)}'`).join(', ')}]`);
|
|
337
|
-
}
|
|
338
|
-
parts.push(`to: '${esc(trans.to)}'`);
|
|
339
|
-
if (trans.roles && trans.roles.length > 0) {
|
|
340
|
-
parts.push(`roles: [${trans.roles.map(r => `'${esc(r)}'`).join(', ')}]`);
|
|
341
|
-
}
|
|
342
|
-
if (trans.auto) {
|
|
343
|
-
parts.push(`auto: true`);
|
|
344
|
-
}
|
|
345
|
-
if (trans.required_fields && trans.required_fields.length > 0) {
|
|
346
|
-
parts.push(`required_fields: [${trans.required_fields.map(f => `'${esc(f)}'`).join(', ')}]`);
|
|
347
|
-
}
|
|
348
|
-
lines.push(` ${safeKey(trans.name)}: { ${parts.join(', ')} },`);
|
|
349
|
-
}
|
|
350
|
-
lines.push(` },`);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
lines.push(`});`);
|
|
354
|
-
lines.push(``);
|
|
355
|
-
|
|
356
|
-
return lines.join('\n');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// =============================================================================
|
|
360
|
-
// Layout File (with auth guards)
|
|
361
|
-
// =============================================================================
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Generates app/layout.tsx with role-based auth guards.
|
|
365
|
-
*/
|
|
366
|
-
function generateLayoutFile(
|
|
367
|
-
slug: string,
|
|
368
|
-
roles: Array<{ name: string; permissions: string[] }>,
|
|
369
|
-
): string {
|
|
370
|
-
const componentName = `${pascalCase(slug)}Layout`;
|
|
371
|
-
const lines: string[] = [
|
|
372
|
-
`/**`,
|
|
373
|
-
` * ${componentName} — root layout with auth guards.`,
|
|
374
|
-
` */`,
|
|
375
|
-
``,
|
|
376
|
-
`import { useRole, Stack } from '@mindmatrix/react';`,
|
|
377
|
-
``,
|
|
378
|
-
`interface LayoutProps {`,
|
|
379
|
-
` children: React.ReactNode;`,
|
|
380
|
-
`}`,
|
|
381
|
-
``,
|
|
382
|
-
`export default function ${componentName}({ children }: LayoutProps) {`,
|
|
383
|
-
];
|
|
384
|
-
|
|
385
|
-
// Role guard declarations
|
|
386
|
-
for (const role of roles) {
|
|
387
|
-
const varName = `is${pascalCase(role.name)}`;
|
|
388
|
-
lines.push(` const ${varName} = useRole('${esc(role.name)}');`);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (roles.length > 0) {
|
|
392
|
-
lines.push(``);
|
|
393
|
-
// Generate a simple authorized check
|
|
394
|
-
const roleChecks = roles.map(r => `is${pascalCase(r.name)}`).join(' || ');
|
|
395
|
-
lines.push(` const isAuthorized = ${roleChecks};`);
|
|
396
|
-
lines.push(``);
|
|
397
|
-
lines.push(` if (!isAuthorized) {`);
|
|
398
|
-
lines.push(` return <Stack sx={{ p: 32, align: 'center' }}>Access denied</Stack>;`);
|
|
399
|
-
lines.push(` }`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
lines.push(``);
|
|
403
|
-
lines.push(` return (`);
|
|
404
|
-
lines.push(` <Stack sx={{ minH: '100vh' }}>`);
|
|
405
|
-
lines.push(` {children}`);
|
|
406
|
-
lines.push(` </Stack>`);
|
|
407
|
-
lines.push(` );`);
|
|
408
|
-
lines.push(`}`);
|
|
409
|
-
lines.push(``);
|
|
410
|
-
|
|
411
|
-
return lines.join('\n');
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// =============================================================================
|
|
415
|
-
// Route Table & Page Classification
|
|
416
|
-
// =============================================================================
|
|
417
|
-
|
|
418
|
-
interface RouteEntry {
|
|
419
|
-
stateName: string;
|
|
420
|
-
role: string;
|
|
421
|
-
page: string;
|
|
422
|
-
path: string;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
interface PageSection {
|
|
426
|
-
route: string;
|
|
427
|
-
role: string;
|
|
428
|
-
slug: string;
|
|
429
|
-
title: string;
|
|
430
|
-
componentName: string;
|
|
431
|
-
tree: IRExperienceNode;
|
|
432
|
-
filePath: string;
|
|
433
|
-
localDefaults: Record<string, unknown>;
|
|
434
|
-
roleGuard?: string;
|
|
435
|
-
dataSources?: unknown[];
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Extracts route table from a router child definition.
|
|
440
|
-
* Parses state descriptions like "Route: /admin/analytics" into structured entries.
|
|
441
|
-
*/
|
|
442
|
-
function extractRouteTable(childDefinitions?: DecompilerInput[]): RouteEntry[] {
|
|
443
|
-
if (!childDefinitions) return [];
|
|
444
|
-
const router = childDefinitions.find(
|
|
445
|
-
c => c.slug.endsWith('-router') || c.category === 'router',
|
|
446
|
-
);
|
|
447
|
-
if (!router?.states) return [];
|
|
448
|
-
|
|
449
|
-
return router.states
|
|
450
|
-
.filter(s => s.description?.startsWith('Route:'))
|
|
451
|
-
.map(s => {
|
|
452
|
-
const route = s.description!.replace(/^Route:\s*/, '').replace(/^\//, '');
|
|
453
|
-
const parts = route.split('/');
|
|
454
|
-
return {
|
|
455
|
-
stateName: s.name,
|
|
456
|
-
role: parts[0] || 'shared',
|
|
457
|
-
page: parts.slice(1).join('/') || parts[0],
|
|
458
|
-
path: route,
|
|
459
|
-
};
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* BFS search for the first h2 heading text in a subtree.
|
|
465
|
-
*/
|
|
466
|
-
function findPageTitle(node: IRExperienceNode, maxDepth = 5): string | null {
|
|
467
|
-
const queue: Array<{ n: IRExperienceNode; d: number }> = [{ n: node, d: 0 }];
|
|
468
|
-
|
|
469
|
-
while (queue.length > 0) {
|
|
470
|
-
const { n, d } = queue.shift()!;
|
|
471
|
-
if (d > maxDepth) continue;
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
(n.component === 'Text' || n.component === 'Heading') &&
|
|
475
|
-
(n.config?.level === 2 || n.config?.variant === 'h2')
|
|
476
|
-
) {
|
|
477
|
-
if (n.config?.text) return n.config.text as string;
|
|
478
|
-
// Composite title from children (e.g., "Payment #" + "{id}")
|
|
479
|
-
if (n.children?.length) {
|
|
480
|
-
const parts = n.children
|
|
481
|
-
.filter(c => c.config?.value)
|
|
482
|
-
.map(c => c.config!.value as string);
|
|
483
|
-
if (parts.length > 0) return parts.join('');
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (n.children) {
|
|
488
|
-
for (const child of n.children) {
|
|
489
|
-
queue.push({ n: child, d: d + 1 });
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
return null;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Detects the role for a page from bindings, localDefaults keys, and title hints.
|
|
499
|
-
*/
|
|
500
|
-
function detectPageRole(node: IRExperienceNode): string {
|
|
501
|
-
// Show when binding
|
|
502
|
-
const when = node.bindings?.when || '';
|
|
503
|
-
if (/isAdmin|is_admin/i.test(when)) return 'admin';
|
|
504
|
-
if (/isDriver|is_driver/i.test(when)) return 'driver';
|
|
505
|
-
if (/isRider|is_rider/i.test(when)) return 'rider';
|
|
506
|
-
|
|
507
|
-
// localDefaults key prefixes
|
|
508
|
-
const ld = (node.config?.localDefaults || {}) as Record<string, unknown>;
|
|
509
|
-
const keys = Object.keys(ld);
|
|
510
|
-
if (keys.some(k => k.startsWith('driver.') || k === 'is_online')) return 'driver';
|
|
511
|
-
if (keys.some(k => k.startsWith('earnings.') || k.startsWith('matching.'))) return 'driver';
|
|
512
|
-
if (keys.some(k => k.startsWith('rider.'))) return 'rider';
|
|
513
|
-
|
|
514
|
-
// localDefaults content-based hints
|
|
515
|
-
if (keys.includes('ride.current_fare') || keys.includes('time_left')) return 'driver';
|
|
516
|
-
if (keys.includes('selected_vehicle') || keys.includes('driver_location')) return 'rider';
|
|
517
|
-
if (keys.includes('date_from') || keys.includes('date_to')) return 'rider';
|
|
518
|
-
if (keys.includes('cvv') || keys.includes('card_number')) return 'rider';
|
|
519
|
-
|
|
520
|
-
// Title-based
|
|
521
|
-
const title = findPageTitle(node);
|
|
522
|
-
if (title) {
|
|
523
|
-
const lower = title.toLowerCase();
|
|
524
|
-
if (lower.includes('driver') || lower.includes('earning') || lower.includes('navigation')) return 'driver';
|
|
525
|
-
if (lower.includes('ride request') || lower.includes('ride acceptance')) return 'driver';
|
|
526
|
-
if (lower.includes('payment method') || lower.includes('ride history')) return 'rider';
|
|
527
|
-
if (lower.includes('your ride') || lower.includes('where to') || lower.includes('ride tracking')) return 'rider';
|
|
528
|
-
if (lower.includes('analytics') || lower.includes('surge') || lower.includes('fleet')) return 'admin';
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
return 'shared';
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Checks if a node represents a child workflow view (not a router page).
|
|
536
|
-
* Detected by title containing "#" (instance refs) or EXACT slug match with child defs.
|
|
537
|
-
* Uses strict matching to avoid false positives (e.g., "Incoming Ride Request" ≠ "ride-request").
|
|
538
|
-
*/
|
|
539
|
-
function isChildWorkflowView(
|
|
540
|
-
node: IRExperienceNode,
|
|
541
|
-
childSlugs: Set<string>,
|
|
542
|
-
): boolean {
|
|
543
|
-
const title = findPageTitle(node);
|
|
544
|
-
if (!title) return false;
|
|
545
|
-
|
|
546
|
-
// Title contains "#" → references specific instance (e.g., "Payment #abc123")
|
|
547
|
-
if (title.includes('#')) return true;
|
|
548
|
-
|
|
549
|
-
// Exact slug match only (no substring matching to avoid false positives)
|
|
550
|
-
const titleSlug = title
|
|
551
|
-
.toLowerCase()
|
|
552
|
-
.replace(/[^a-z0-9\s]/g, '')
|
|
553
|
-
.trim()
|
|
554
|
-
.replace(/\s+/g, '-');
|
|
555
|
-
|
|
556
|
-
return childSlugs.has(titleSlug);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/** Converts a page title to a URL-friendly slug. */
|
|
560
|
-
function titleToSlug(title: string): string {
|
|
561
|
-
const MAP: Record<string, string> = {
|
|
562
|
-
'where to?': 'home',
|
|
563
|
-
'where to': 'home',
|
|
564
|
-
'your ride': 'ride-tracking',
|
|
565
|
-
'incoming ride request': 'ride-acceptance',
|
|
566
|
-
};
|
|
567
|
-
const lower = title.toLowerCase();
|
|
568
|
-
if (MAP[lower]) return MAP[lower];
|
|
569
|
-
|
|
570
|
-
return lower
|
|
571
|
-
.replace(/[^a-z0-9\s]/g, '')
|
|
572
|
-
.trim()
|
|
573
|
-
.replace(/\s+/g, '-');
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/** Scores how well a page title matches a route entry. */
|
|
577
|
-
function scoreRouteMatch(title: string, route: RouteEntry): number {
|
|
578
|
-
const titleSlug = title
|
|
579
|
-
.toLowerCase()
|
|
580
|
-
.replace(/[^a-z0-9\s]/g, '')
|
|
581
|
-
.trim()
|
|
582
|
-
.replace(/\s+/g, '-');
|
|
583
|
-
const titleWords = new Set(titleSlug.split('-').filter(w => w.length > 2));
|
|
584
|
-
|
|
585
|
-
let score = 0;
|
|
586
|
-
for (const word of route.page.split('-')) {
|
|
587
|
-
if (titleWords.has(word)) score += 2;
|
|
588
|
-
}
|
|
589
|
-
if (titleSlug.includes(route.page) || route.page.includes(titleSlug)) {
|
|
590
|
-
score += 3;
|
|
591
|
-
}
|
|
592
|
-
return score;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
/** Extracts role guard binding from a Show node (e.g., "isAdmin"). */
|
|
596
|
-
function extractRoleGuard(node: IRExperienceNode): string | undefined {
|
|
597
|
-
const when = node.bindings?.when || '';
|
|
598
|
-
const match = when.match(/\$instance\.(is\w+)/);
|
|
599
|
-
return match ? match[1] : undefined;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/** Collects dataSources from a node and its immediate children. */
|
|
603
|
-
function collectDataSources(node: IRExperienceNode): unknown[] {
|
|
604
|
-
const sources: unknown[] = [];
|
|
605
|
-
const nodeDS = (node as unknown as Record<string, unknown>).dataSources;
|
|
606
|
-
if (Array.isArray(nodeDS)) sources.push(...nodeDS);
|
|
607
|
-
|
|
608
|
-
if (node.children) {
|
|
609
|
-
for (const child of node.children) {
|
|
610
|
-
const childDS = (child as unknown as Record<string, unknown>).dataSources;
|
|
611
|
-
if (Array.isArray(childDS)) sources.push(...childDS);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
return sources;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/** Strips localDefaults from a node's config (shallow copy). */
|
|
618
|
-
function stripLocalDefaults(node: IRExperienceNode): IRExperienceNode {
|
|
619
|
-
if (!node.config?.localDefaults) return node;
|
|
620
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
621
|
-
const { localDefaults, ...rest } = node.config;
|
|
622
|
-
return { ...node, config: Object.keys(rest).length > 0 ? rest : undefined };
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/** Gets page content tree: unwraps Show wrappers, removes localDefaults. */
|
|
626
|
-
function getPageContentTree(node: IRExperienceNode): IRExperienceNode {
|
|
627
|
-
if (node.component === 'Show' && node.children?.length === 1) {
|
|
628
|
-
return stripLocalDefaults(node.children[0]);
|
|
629
|
-
}
|
|
630
|
-
return stripLocalDefaults(node);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/** Infers a field type from a default value. */
|
|
634
|
-
function inferFieldType(value: unknown): string {
|
|
635
|
-
if (typeof value === 'boolean') return 'boolean';
|
|
636
|
-
if (typeof value === 'number') return 'number';
|
|
637
|
-
return 'text';
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Main page extraction: walks root experience children, classifies them as
|
|
642
|
-
* route pages or child workflow views, and matches pages to router states.
|
|
643
|
-
*
|
|
644
|
-
* Two-pass route matching:
|
|
645
|
-
* Pass 1: High-confidence keyword overlap (score >= 3)
|
|
646
|
-
* Pass 2: Assign remaining by elimination within each role group
|
|
647
|
-
*/
|
|
648
|
-
function extractPageSections(
|
|
649
|
-
experience: IRExperienceNode | undefined,
|
|
650
|
-
childDefinitions?: DecompilerInput[],
|
|
651
|
-
): { pages: PageSection[]; childViews: IRExperienceNode[] } {
|
|
652
|
-
if (!experience?.children) return { pages: [], childViews: [] };
|
|
653
|
-
|
|
654
|
-
// ── Fast path: Router/Route experience (already structured by compiler) ──
|
|
655
|
-
// When the experience already contains a Router with Route children (e.g. from
|
|
656
|
-
// a previous compile→decompile cycle), extract pages deterministically from the
|
|
657
|
-
// Route nodes. This ensures source stabilization (pass N+1 = pass N).
|
|
658
|
-
const routerExtracted = extractFromRouterExperience(experience);
|
|
659
|
-
if (routerExtracted) return routerExtracted;
|
|
660
|
-
|
|
661
|
-
// ── Heuristic path: flat experience (original source) ──
|
|
662
|
-
const routeTable = extractRouteTable(childDefinitions);
|
|
663
|
-
const childSlugs = new Set(
|
|
664
|
-
(childDefinitions || [])
|
|
665
|
-
.filter(c => !c.slug.endsWith('-router') && c.category !== 'router')
|
|
666
|
-
.map(c => c.slug),
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
const pages: PageSection[] = [];
|
|
670
|
-
const childViews: IRExperienceNode[] = [];
|
|
671
|
-
const candidates: Array<{
|
|
672
|
-
title: string | null;
|
|
673
|
-
role: string;
|
|
674
|
-
index: number;
|
|
675
|
-
node: IRExperienceNode;
|
|
676
|
-
localDefaults: Record<string, unknown>;
|
|
677
|
-
roleGuard?: string;
|
|
678
|
-
dataSources?: unknown[];
|
|
679
|
-
}> = [];
|
|
680
|
-
|
|
681
|
-
// Phase 1: classify each child
|
|
682
|
-
for (let i = 0; i < experience.children.length; i++) {
|
|
683
|
-
const child = experience.children[i];
|
|
684
|
-
|
|
685
|
-
if (isChildWorkflowView(child, childSlugs)) {
|
|
686
|
-
childViews.push(child);
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const title = findPageTitle(child);
|
|
691
|
-
const role = detectPageRole(child);
|
|
692
|
-
const localDefaults = (child.config?.localDefaults || {}) as Record<string, unknown>;
|
|
693
|
-
const roleGuard = extractRoleGuard(child);
|
|
694
|
-
const dataSources = collectDataSources(child);
|
|
695
|
-
|
|
696
|
-
candidates.push({
|
|
697
|
-
title, role, index: i, node: child,
|
|
698
|
-
localDefaults, roleGuard,
|
|
699
|
-
dataSources: dataSources.length > 0 ? dataSources : undefined,
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// Phase 2: match to routes (two-pass)
|
|
704
|
-
const assignments = new Map<number, RouteEntry>();
|
|
705
|
-
const usedRoutes = new Set<string>();
|
|
706
|
-
|
|
707
|
-
// Pass 1: high-confidence keyword matches
|
|
708
|
-
for (const cand of candidates) {
|
|
709
|
-
if (!cand.title) continue;
|
|
710
|
-
const roleRoutes = routeTable.filter(
|
|
711
|
-
r => r.role === cand.role && !usedRoutes.has(r.stateName),
|
|
712
|
-
);
|
|
713
|
-
let bestRoute: RouteEntry | null = null;
|
|
714
|
-
let bestScore = 0;
|
|
715
|
-
for (const route of roleRoutes) {
|
|
716
|
-
const score = scoreRouteMatch(cand.title, route);
|
|
717
|
-
if (score > bestScore) { bestScore = score; bestRoute = route; }
|
|
718
|
-
}
|
|
719
|
-
if (bestRoute && bestScore >= 3) {
|
|
720
|
-
assignments.set(cand.index, bestRoute);
|
|
721
|
-
usedRoutes.add(bestRoute.stateName);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Pass 2: iterative elimination — loop until no more assignments made.
|
|
726
|
-
// This ensures that after a titled candidate consumes a route, title-less
|
|
727
|
-
// candidates that now have only 1 remaining route in their role group get assigned.
|
|
728
|
-
let changed = true;
|
|
729
|
-
while (changed) {
|
|
730
|
-
changed = false;
|
|
731
|
-
for (const cand of candidates) {
|
|
732
|
-
if (assignments.has(cand.index)) continue;
|
|
733
|
-
const remaining = routeTable.filter(
|
|
734
|
-
r => r.role === cand.role && !usedRoutes.has(r.stateName),
|
|
735
|
-
);
|
|
736
|
-
if (remaining.length === 1) {
|
|
737
|
-
assignments.set(cand.index, remaining[0]);
|
|
738
|
-
usedRoutes.add(remaining[0].stateName);
|
|
739
|
-
changed = true;
|
|
740
|
-
} else if (remaining.length > 1 && cand.title) {
|
|
741
|
-
let bestRoute: RouteEntry | null = null;
|
|
742
|
-
let bestScore = 0;
|
|
743
|
-
for (const route of remaining) {
|
|
744
|
-
const score = scoreRouteMatch(cand.title, route);
|
|
745
|
-
if (score > bestScore) { bestScore = score; bestRoute = route; }
|
|
746
|
-
}
|
|
747
|
-
if (bestRoute && bestScore > 0) {
|
|
748
|
-
assignments.set(cand.index, bestRoute);
|
|
749
|
-
usedRoutes.add(bestRoute.stateName);
|
|
750
|
-
changed = true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Phase 3: build PageSection array
|
|
757
|
-
for (const cand of candidates) {
|
|
758
|
-
const route = assignments.get(cand.index);
|
|
759
|
-
const slug = route?.page || titleToSlug(cand.title || `page-${cand.index}`);
|
|
760
|
-
const filePath = `app/${cand.role}/${slug}.tsx`;
|
|
761
|
-
const componentName = pascalCase(slug) + 'Page';
|
|
762
|
-
|
|
763
|
-
pages.push({
|
|
764
|
-
route: route?.path || `${cand.role}/${slug}`,
|
|
765
|
-
role: cand.role,
|
|
766
|
-
slug,
|
|
767
|
-
title: cand.title || slug,
|
|
768
|
-
componentName,
|
|
769
|
-
tree: cand.node,
|
|
770
|
-
filePath,
|
|
771
|
-
localDefaults: cand.localDefaults,
|
|
772
|
-
roleGuard: cand.roleGuard,
|
|
773
|
-
dataSources: cand.dataSources,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return { pages, childViews };
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Extracts pages from a Router/Route experience tree produced by the compiler.
|
|
782
|
-
* Returns null if the experience is not a Router/Route structure.
|
|
783
|
-
*
|
|
784
|
-
* Router experience layout:
|
|
785
|
-
* Stack(blueprint-root)
|
|
786
|
-
* Row(nav-bar) → NavLink children
|
|
787
|
-
* Router(page-router) → Route children, each wrapping a page tree
|
|
788
|
-
*/
|
|
789
|
-
function extractFromRouterExperience(
|
|
790
|
-
experience: IRExperienceNode,
|
|
791
|
-
): { pages: PageSection[]; childViews: IRExperienceNode[] } | null {
|
|
792
|
-
if (!experience.children) return null;
|
|
793
|
-
|
|
794
|
-
// Find the Router node among experience children
|
|
795
|
-
const routerNode = experience.children.find(c => c.component === 'Router');
|
|
796
|
-
if (!routerNode?.children) return null;
|
|
797
|
-
|
|
798
|
-
// Collect Route children (skip the index/exact duplicate)
|
|
799
|
-
const routes = routerNode.children.filter(c => c.component === 'Route');
|
|
800
|
-
if (routes.length === 0) return null;
|
|
801
|
-
|
|
802
|
-
const pages: PageSection[] = [];
|
|
803
|
-
const seenPaths = new Set<string>();
|
|
804
|
-
|
|
805
|
-
for (const route of routes) {
|
|
806
|
-
const routePath = (route.config?.path as string) || '';
|
|
807
|
-
// Skip index route (exact: true, path: '/') — it's a duplicate of the first named route
|
|
808
|
-
if (route.config?.exact && routePath === '/') continue;
|
|
809
|
-
if (seenPaths.has(routePath)) continue;
|
|
810
|
-
seenPaths.add(routePath);
|
|
811
|
-
|
|
812
|
-
// Derive slug and role from route path: /setup/review → role=setup, slug=review
|
|
813
|
-
// Single-segment routes (e.g. /page) go directly under app/ to avoid the
|
|
814
|
-
// compiler's /page suffix stripping creating a path mismatch on round-trip.
|
|
815
|
-
const segments = routePath.replace(/^\//, '').split('/').filter(Boolean);
|
|
816
|
-
const role = segments.length > 1 ? segments[0] : undefined;
|
|
817
|
-
const slug = segments.length > 1 ? segments.slice(1).join('-') : (segments[0] || 'page0');
|
|
818
|
-
const filePath = role ? `app/${role}/${slug}.tsx` : `app/${slug}.tsx`;
|
|
819
|
-
const componentName = pascalCase(slug) + 'Page';
|
|
820
|
-
|
|
821
|
-
// The page content is the Route's child (unwrap the Route wrapper).
|
|
822
|
-
// Strip compiler-added gap/padding config that the project-compiler injects
|
|
823
|
-
// on Route children (config: { ...child.config, gap: 4, padding: 4 }).
|
|
824
|
-
const rawPageTree = route.children?.[0] || route;
|
|
825
|
-
const pageTree = { ...rawPageTree };
|
|
826
|
-
if (pageTree.config) {
|
|
827
|
-
const { gap, padding, ...restConfig } = pageTree.config as Record<string, unknown>;
|
|
828
|
-
pageTree.config = Object.keys(restConfig).length > 0 ? restConfig : undefined;
|
|
829
|
-
}
|
|
830
|
-
const localDefaults = (pageTree.config?.localDefaults || {}) as Record<string, unknown>;
|
|
831
|
-
|
|
832
|
-
pages.push({
|
|
833
|
-
route: routePath,
|
|
834
|
-
role: role || '',
|
|
835
|
-
slug,
|
|
836
|
-
title: findPageTitle(pageTree) || slug,
|
|
837
|
-
componentName,
|
|
838
|
-
tree: pageTree,
|
|
839
|
-
filePath,
|
|
840
|
-
localDefaults,
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return pages.length > 0 ? { pages, childViews: [] } : null;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Generates a standalone page component file from a PageSection.
|
|
849
|
-
* Converts localDefaults to useState, unwraps the tree, includes data sources.
|
|
850
|
-
* When model slugs are referenced via dataSources, emits typed model imports.
|
|
851
|
-
*/
|
|
852
|
-
function generatePageFileFromSection(
|
|
853
|
-
page: PageSection,
|
|
854
|
-
modelSlugToPath?: Map<string, string>,
|
|
855
|
-
): string {
|
|
856
|
-
const fields: IRFieldDefinition[] = Object.entries(page.localDefaults)
|
|
857
|
-
.filter(([key]) => !key.includes('.'))
|
|
858
|
-
.map(([key, value]) => ({
|
|
859
|
-
name: key,
|
|
860
|
-
type: inferFieldType(value),
|
|
861
|
-
required: false,
|
|
862
|
-
default_value: value ?? undefined,
|
|
863
|
-
}));
|
|
864
|
-
|
|
865
|
-
const contentTree = getPageContentTree(page.tree);
|
|
866
|
-
|
|
867
|
-
const metadata: Record<string, unknown> = {};
|
|
868
|
-
if (page.dataSources && page.dataSources.length > 0) {
|
|
869
|
-
metadata.dataSources = page.dataSources;
|
|
870
|
-
|
|
871
|
-
// Build model imports map: slug → relative import path from this page file
|
|
872
|
-
if (modelSlugToPath) {
|
|
873
|
-
const modelImports: Record<string, string> = {};
|
|
874
|
-
for (const ds of page.dataSources) {
|
|
875
|
-
const dsObj = ds as Record<string, unknown>;
|
|
876
|
-
if (dsObj.type !== 'workflow') continue;
|
|
877
|
-
const slug = (dsObj.slug || dsObj.name) as string;
|
|
878
|
-
if (!slug) continue;
|
|
879
|
-
const modelPath = modelSlugToPath.get(slug);
|
|
880
|
-
if (modelPath) {
|
|
881
|
-
// Compute relative path from page file to model file
|
|
882
|
-
const pageDir = page.filePath.split('/').slice(0, -1).join('/');
|
|
883
|
-
const rel = computeRelativeImport(pageDir, modelPath);
|
|
884
|
-
modelImports[slug] = rel;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
if (Object.keys(modelImports).length > 0) {
|
|
888
|
-
metadata.modelImports = modelImports;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const pageInput: DecompilerInput = {
|
|
894
|
-
slug: page.slug,
|
|
895
|
-
name: page.componentName,
|
|
896
|
-
version: '1.0.0',
|
|
897
|
-
category: 'page',
|
|
898
|
-
states: [],
|
|
899
|
-
transitions: [],
|
|
900
|
-
fields,
|
|
901
|
-
roles: [],
|
|
902
|
-
experience: contentTree,
|
|
903
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
904
|
-
};
|
|
905
|
-
|
|
906
|
-
return decompile(pageInput, {
|
|
907
|
-
componentName: page.componentName,
|
|
908
|
-
includeAnnotation: false,
|
|
909
|
-
}).code;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Computes a relative import path from a source directory to a target file.
|
|
914
|
-
* E.g. from "app/shared" to "models/task.ts" → "../../models/task"
|
|
915
|
-
*/
|
|
916
|
-
function computeRelativeImport(fromDir: string, toFile: string): string {
|
|
917
|
-
const fromParts = fromDir.split('/').filter(Boolean);
|
|
918
|
-
const toParts = toFile.replace(/\.(ts|tsx)$/, '').split('/').filter(Boolean);
|
|
919
|
-
|
|
920
|
-
// Find common prefix length
|
|
921
|
-
let common = 0;
|
|
922
|
-
while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
|
|
923
|
-
common++;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const ups = fromParts.length - common;
|
|
927
|
-
const downs = toParts.slice(common);
|
|
928
|
-
|
|
929
|
-
if (ups === 0 && downs.length === 0) return './index';
|
|
930
|
-
const prefix = ups > 0 ? '../'.repeat(ups) : './';
|
|
931
|
-
return prefix + downs.join('/');
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* Generates a main.workflow.tsx that imports page components and composes them.
|
|
936
|
-
* Keeps global hooks from the definition and child workflow views inline.
|
|
937
|
-
*/
|
|
938
|
-
function generateMainWithPages(
|
|
939
|
-
definition: DecompilerInput,
|
|
940
|
-
pages: PageSection[],
|
|
941
|
-
childViews: IRExperienceNode[],
|
|
942
|
-
): string {
|
|
943
|
-
// Build modified experience tree with page component refs
|
|
944
|
-
const mainChildren: IRExperienceNode[] = [];
|
|
945
|
-
|
|
946
|
-
for (const page of pages) {
|
|
947
|
-
if (page.roleGuard) {
|
|
948
|
-
mainChildren.push({
|
|
949
|
-
id: `page-${page.slug}`,
|
|
950
|
-
component: 'Show',
|
|
951
|
-
bindings: { when: `$instance.${page.roleGuard}` },
|
|
952
|
-
children: [{ id: `${page.slug}-ref`, component: page.componentName }],
|
|
953
|
-
});
|
|
954
|
-
} else {
|
|
955
|
-
mainChildren.push({
|
|
956
|
-
id: `page-${page.slug}`,
|
|
957
|
-
component: page.componentName,
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
for (const cv of childViews) {
|
|
963
|
-
mainChildren.push(cv);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const mainTree: IRExperienceNode = {
|
|
967
|
-
id: 'root',
|
|
968
|
-
component: 'Stack',
|
|
969
|
-
children: mainChildren,
|
|
970
|
-
};
|
|
971
|
-
|
|
972
|
-
// Strip dataSources from metadata for the main file — they belong to
|
|
973
|
-
// individual page files, not the top-level orchestrator. Including them here
|
|
974
|
-
// causes import instability (useMutation/useQuery present on pass 1 but not pass 2).
|
|
975
|
-
const strippedMeta = { ...(definition.metadata || {}) };
|
|
976
|
-
delete (strippedMeta as any).dataSources;
|
|
977
|
-
delete (strippedMeta as any).queries;
|
|
978
|
-
delete (strippedMeta as any).mutations;
|
|
979
|
-
delete (strippedMeta as any).mutationTargets;
|
|
980
|
-
const modifiedDef: DecompilerInput = {
|
|
981
|
-
...definition,
|
|
982
|
-
experience: mainTree,
|
|
983
|
-
metadata: strippedMeta,
|
|
984
|
-
};
|
|
985
|
-
const result = decompile(modifiedDef, { includeAnnotation: true });
|
|
986
|
-
|
|
987
|
-
// Build import lines for page components
|
|
988
|
-
const pageImports = pages
|
|
989
|
-
.map(p => `import ${p.componentName} from './${p.filePath.replace(/\.tsx$/, '')}';`)
|
|
990
|
-
.join('\n');
|
|
991
|
-
|
|
992
|
-
// Insert after last existing import line
|
|
993
|
-
const code = result.code;
|
|
994
|
-
const importRegex = /^import .+$/gm;
|
|
995
|
-
let lastImportEnd = -1;
|
|
996
|
-
let m: RegExpExecArray | null;
|
|
997
|
-
while ((m = importRegex.exec(code)) !== null) {
|
|
998
|
-
lastImportEnd = m.index + m[0].length;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (lastImportEnd > 0) {
|
|
1002
|
-
return code.slice(0, lastImportEnd) + '\n' + pageImports + code.slice(lastImportEnd);
|
|
1003
|
-
}
|
|
1004
|
-
return pageImports + '\n' + code;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
// =============================================================================
|
|
1008
|
-
// Server Action Extraction
|
|
1009
|
-
// =============================================================================
|
|
1010
|
-
|
|
1011
|
-
interface ServerAction {
|
|
1012
|
-
name: string;
|
|
1013
|
-
type: string;
|
|
1014
|
-
config: Record<string, unknown>;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
const SERVER_ACTION_TYPES = new Set([
|
|
1018
|
-
'http_request', 'webhook', 'call_webhook',
|
|
1019
|
-
'notify', 'send_notification',
|
|
1020
|
-
'call_workflow', 'spawn_instance', 'spawn_subworkflow',
|
|
1021
|
-
'emit_event',
|
|
1022
|
-
'custom',
|
|
1023
|
-
]);
|
|
1024
|
-
|
|
1025
|
-
function extractServerActions(
|
|
1026
|
-
states: IRStateDefinition[],
|
|
1027
|
-
transitions: IRTransitionDefinition[],
|
|
1028
|
-
): ServerAction[] {
|
|
1029
|
-
const seen = new Set<string>();
|
|
1030
|
-
const actions: ServerAction[] = [];
|
|
1031
|
-
|
|
1032
|
-
function collect(defs: Array<{ id: string; type: string; config: Record<string, unknown> }>) {
|
|
1033
|
-
for (const action of defs) {
|
|
1034
|
-
if (SERVER_ACTION_TYPES.has(action.type) && !seen.has(action.id)) {
|
|
1035
|
-
seen.add(action.id);
|
|
1036
|
-
const slug = String(action.config.name || action.config.event || action.config.slug || action.type);
|
|
1037
|
-
const name = camelCase(slug.replace(/[^a-zA-Z0-9_-]/g, '-'));
|
|
1038
|
-
actions.push({ name, type: action.type, config: action.config });
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
for (const state of states) {
|
|
1044
|
-
collect(state.on_enter);
|
|
1045
|
-
collect(state.on_exit);
|
|
1046
|
-
for (const during of state.during) {
|
|
1047
|
-
collect(during.actions);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
for (const trans of transitions) {
|
|
1051
|
-
collect(trans.actions);
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
return actions;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
function generateServerActionFile(
|
|
1058
|
-
slug: string,
|
|
1059
|
-
actions: ServerAction[],
|
|
1060
|
-
): string {
|
|
1061
|
-
const lines: string[] = [
|
|
1062
|
-
`/**`,
|
|
1063
|
-
` * Server actions for "${slug}".`,
|
|
1064
|
-
` *`,
|
|
1065
|
-
` * These run server-side during state transitions.`,
|
|
1066
|
-
` */`,
|
|
1067
|
-
``,
|
|
1068
|
-
`import type { TransitionContext } from '@mindmatrix/react';`,
|
|
1069
|
-
``,
|
|
1070
|
-
];
|
|
1071
|
-
|
|
1072
|
-
for (const action of actions) {
|
|
1073
|
-
lines.push(`/** ${actionComment(action)} */`);
|
|
1074
|
-
lines.push(`export async function ${action.name}(ctx: TransitionContext): Promise<void> {`);
|
|
1075
|
-
lines.push(` const { instance, env } = ctx;`);
|
|
1076
|
-
|
|
1077
|
-
switch (action.type) {
|
|
1078
|
-
case 'http_request': case 'webhook': case 'call_webhook': {
|
|
1079
|
-
const url = String(action.config.url || 'https://api.example.com/webhook');
|
|
1080
|
-
const method = String(action.config.method || 'POST');
|
|
1081
|
-
lines.push(` await fetch('${esc(url)}', {`);
|
|
1082
|
-
lines.push(` method: '${method}',`);
|
|
1083
|
-
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
1084
|
-
lines.push(` body: JSON.stringify({ instanceId: instance.id }),`);
|
|
1085
|
-
lines.push(` });`);
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
case 'notify': case 'send_notification': {
|
|
1089
|
-
const msg = String(action.config.message || action.config.body || 'Notification');
|
|
1090
|
-
lines.push(` await env.notify({ message: '${esc(msg)}', instanceId: instance.id });`);
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
case 'call_workflow': case 'spawn_instance': case 'spawn_subworkflow': {
|
|
1094
|
-
const target = String(action.config.slug || action.config.workflow || 'child-workflow');
|
|
1095
|
-
lines.push(` await env.spawn('${esc(target)}', { parentId: instance.id });`);
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
case 'emit_event': {
|
|
1099
|
-
const event = String(action.config.event || action.config.name || 'custom-event');
|
|
1100
|
-
lines.push(` await env.emit('${esc(event)}', { instanceId: instance.id });`);
|
|
1101
|
-
break;
|
|
1102
|
-
}
|
|
1103
|
-
case 'custom': {
|
|
1104
|
-
const expr = action.config.expression || action.config.body;
|
|
1105
|
-
if (expr) {
|
|
1106
|
-
lines.push(` // Original expression: ${String(expr).replace(/\n/g, '\\n').slice(0, 200)}`);
|
|
1107
|
-
}
|
|
1108
|
-
lines.push(` // TODO: Implement custom action logic`);
|
|
1109
|
-
lines.push(` throw new Error('Not implemented — decompiled stub');`);
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
default:
|
|
1113
|
-
lines.push(` // TODO: Implement ${action.type} logic`);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
lines.push(`}`);
|
|
1117
|
-
lines.push(``);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
return lines.join('\n');
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function actionComment(action: ServerAction): string {
|
|
1124
|
-
switch (action.type) {
|
|
1125
|
-
case 'http_request': case 'webhook': case 'call_webhook':
|
|
1126
|
-
return `HTTP ${action.config.method || 'POST'} to ${action.config.url || 'webhook'}`;
|
|
1127
|
-
case 'notify': case 'send_notification':
|
|
1128
|
-
return `Send notification: ${action.config.message || ''}`;
|
|
1129
|
-
case 'call_workflow': case 'spawn_instance': case 'spawn_subworkflow':
|
|
1130
|
-
return `Spawn workflow: ${action.config.slug || action.config.workflow || ''}`;
|
|
1131
|
-
case 'emit_event':
|
|
1132
|
-
return `Emit event: ${action.config.event || action.config.name || ''}`;
|
|
1133
|
-
case 'custom':
|
|
1134
|
-
return `Custom action: ${action.name}`;
|
|
1135
|
-
default:
|
|
1136
|
-
return `Server action: ${action.type}`;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// =============================================================================
|
|
1141
|
-
// Component Definition Emission (from metadata.componentDefinitions)
|
|
1142
|
-
// =============================================================================
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Emits component files from metadata.componentDefinitions.
|
|
1146
|
-
* Each entry maps a component name to its experience tree and props.
|
|
1147
|
-
*/
|
|
1148
|
-
function emitComponentDefinitions(
|
|
1149
|
-
definition: DecompilerInput,
|
|
1150
|
-
files: EnhancedProjectFile[],
|
|
1151
|
-
): void {
|
|
1152
|
-
const meta = definition.metadata as Record<string, unknown> | undefined;
|
|
1153
|
-
const componentDefs = meta?.componentDefinitions as
|
|
1154
|
-
Record<string, { experience: IRExperienceNode; props: string[] }> | undefined;
|
|
1155
|
-
if (!componentDefs) return;
|
|
1156
|
-
|
|
1157
|
-
for (const [name, def] of Object.entries(componentDefs)) {
|
|
1158
|
-
const content = generateComponentFromDefinition(name, def.experience, def.props);
|
|
1159
|
-
files.push({
|
|
1160
|
-
path: `components/${name}.tsx`,
|
|
1161
|
-
role: 'component',
|
|
1162
|
-
content,
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
/**
|
|
1168
|
-
* Infers a TypeScript type for a component prop by analyzing how it's used
|
|
1169
|
-
* in the experience tree (bindings, visible_when, event handlers).
|
|
1170
|
-
*/
|
|
1171
|
-
function inferPropType(propName: string, experience: IRExperienceNode): string {
|
|
1172
|
-
// Walk the tree to find usages
|
|
1173
|
-
const usages: string[] = [];
|
|
1174
|
-
collectPropUsages(propName, experience, usages);
|
|
1175
|
-
|
|
1176
|
-
// Event handlers → callback types
|
|
1177
|
-
if (propName.startsWith('on') && propName.length > 2 && propName[2] === propName[2].toUpperCase()) {
|
|
1178
|
-
return '(() => void)';
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
// Boolean-like names
|
|
1182
|
-
const boolNames = ['show', 'is', 'has', 'enable', 'disable', 'compact', 'track', 'follow'];
|
|
1183
|
-
if (boolNames.some(b => propName.toLowerCase().startsWith(b))) return 'boolean';
|
|
1184
|
-
|
|
1185
|
-
// Known patterns from bindings
|
|
1186
|
-
for (const usage of usages) {
|
|
1187
|
-
if (usage.includes('.address') || usage.includes('Address')) return '{ address: string; latitude: number; longitude: number }';
|
|
1188
|
-
if (usage.includes('.toFixed')) return 'number';
|
|
1189
|
-
if (usage.includes('.toLocaleString')) return 'number';
|
|
1190
|
-
if (usage.includes('.heading')) return '{ latitude: number; longitude: number; heading: number }';
|
|
1191
|
-
if (usage.includes('.length')) return 'unknown[]';
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// Name-based heuristics
|
|
1195
|
-
if (/url|src|image|avatar|icon/i.test(propName)) return 'string';
|
|
1196
|
-
if (/count|amount|total|rating|eta|minutes|radius|zoom/i.test(propName)) return 'number';
|
|
1197
|
-
if (/style|sx/i.test(propName)) return 'React.CSSProperties';
|
|
1198
|
-
if (/items|list|zones|options/i.test(propName)) return 'unknown[]';
|
|
1199
|
-
if (/location|position|point/i.test(propName)) return '{ latitude: number; longitude: number }';
|
|
1200
|
-
|
|
1201
|
-
return 'unknown';
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
/** Collects binding expressions that reference a prop name. */
|
|
1205
|
-
function collectPropUsages(propName: string, node: IRExperienceNode, out: string[]): void {
|
|
1206
|
-
if (node.bindings) {
|
|
1207
|
-
for (const expr of Object.values(node.bindings)) {
|
|
1208
|
-
if (typeof expr === 'string' && expr.includes(propName)) {
|
|
1209
|
-
out.push(expr);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
if (node.visible_when && typeof node.visible_when === 'string' && node.visible_when.includes(propName)) {
|
|
1214
|
-
out.push(node.visible_when);
|
|
1215
|
-
}
|
|
1216
|
-
if (node.children) {
|
|
1217
|
-
for (const child of node.children) {
|
|
1218
|
-
collectPropUsages(propName, child, out);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
/**
|
|
1224
|
-
* Generates a component file from an experience tree and prop list.
|
|
1225
|
-
* Produces proper TypeScript interfaces with inferred prop types and
|
|
1226
|
-
* destructured props in the function signature.
|
|
1227
|
-
*/
|
|
1228
|
-
function generateComponentFromDefinition(
|
|
1229
|
-
name: string,
|
|
1230
|
-
experience: IRExperienceNode,
|
|
1231
|
-
props: string[],
|
|
1232
|
-
): string {
|
|
1233
|
-
// Decompile the experience tree into JSX
|
|
1234
|
-
const pageInput: DecompilerInput = {
|
|
1235
|
-
slug: name.toLowerCase(),
|
|
1236
|
-
name,
|
|
1237
|
-
version: '1.0.0',
|
|
1238
|
-
category: 'component',
|
|
1239
|
-
states: [],
|
|
1240
|
-
transitions: [],
|
|
1241
|
-
fields: [],
|
|
1242
|
-
roles: [],
|
|
1243
|
-
experience,
|
|
1244
|
-
};
|
|
1245
|
-
|
|
1246
|
-
const result = decompile(pageInput, {
|
|
1247
|
-
componentName: name,
|
|
1248
|
-
includeAnnotation: false,
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
// If the component has props, inject a typed interface and destructured params
|
|
1252
|
-
if (props.length > 0) {
|
|
1253
|
-
const typedProps = props.map(p => {
|
|
1254
|
-
const type = inferPropType(p, experience);
|
|
1255
|
-
const isCallback = p.startsWith('on') && p.length > 2 && p[2] === p[2].toUpperCase();
|
|
1256
|
-
const optional = isCallback ? '?' : '';
|
|
1257
|
-
return ` ${p}${optional}: ${type};`;
|
|
1258
|
-
});
|
|
1259
|
-
|
|
1260
|
-
const propsInterface = `interface ${name}Props {\n${typedProps.join('\n')}\n}\n\n`;
|
|
1261
|
-
const code = result.code
|
|
1262
|
-
.replace(
|
|
1263
|
-
`export default function ${name}()`,
|
|
1264
|
-
`${propsInterface}export default function ${name}({ ${props.join(', ')} }: ${name}Props)`,
|
|
1265
|
-
);
|
|
1266
|
-
return code;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
return result.code;
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
// =============================================================================
|
|
1273
|
-
// Server Action Emission from Metadata (preserved bodies)
|
|
1274
|
-
// =============================================================================
|
|
1275
|
-
|
|
1276
|
-
/**
|
|
1277
|
-
* Emits server action files from metadata.serverActions when they have
|
|
1278
|
-
* preserved function bodies. Returns the list of emitted file paths.
|
|
1279
|
-
*/
|
|
1280
|
-
function emitServerActionsFromMetadata(
|
|
1281
|
-
definition: DecompilerInput,
|
|
1282
|
-
files: EnhancedProjectFile[],
|
|
1283
|
-
): string[] {
|
|
1284
|
-
const meta = definition.metadata as Record<string, unknown> | undefined;
|
|
1285
|
-
const serverActions = meta?.serverActions as Array<{
|
|
1286
|
-
name: string;
|
|
1287
|
-
sourceFile?: string;
|
|
1288
|
-
body?: string;
|
|
1289
|
-
async?: boolean;
|
|
1290
|
-
params?: string[];
|
|
1291
|
-
description?: string;
|
|
1292
|
-
returnType?: string;
|
|
1293
|
-
}> | undefined;
|
|
1294
|
-
|
|
1295
|
-
if (!serverActions || serverActions.length === 0) return [];
|
|
1296
|
-
|
|
1297
|
-
// Only emit if at least one action has a preserved body
|
|
1298
|
-
const actionsWithBodies = serverActions.filter(a => a.body);
|
|
1299
|
-
if (actionsWithBodies.length === 0) return [];
|
|
1300
|
-
|
|
1301
|
-
// Group actions by source file
|
|
1302
|
-
const byFile = new Map<string, typeof actionsWithBodies>();
|
|
1303
|
-
for (const action of actionsWithBodies) {
|
|
1304
|
-
const file = action.sourceFile || `actions/${action.name}.server.ts`;
|
|
1305
|
-
if (!byFile.has(file)) byFile.set(file, []);
|
|
1306
|
-
byFile.get(file)!.push(action);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
const emittedPaths: string[] = [];
|
|
1310
|
-
for (const [filePath, actions] of byFile) {
|
|
1311
|
-
const content = generateServerActionFileFromBodies(filePath, actions);
|
|
1312
|
-
// Check if a file at this path was already emitted by extractServerActions
|
|
1313
|
-
const existing = files.findIndex(f => f.path === filePath);
|
|
1314
|
-
if (existing !== -1) {
|
|
1315
|
-
// Replace the generated stub with the preserved bodies
|
|
1316
|
-
files[existing].content = content;
|
|
1317
|
-
} else {
|
|
1318
|
-
files.push({ path: filePath, role: 'server-action', content });
|
|
1319
|
-
}
|
|
1320
|
-
emittedPaths.push(filePath);
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
return emittedPaths;
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* Generates a server action file from preserved function bodies.
|
|
1328
|
-
* Includes the standard import header and each function's full source.
|
|
1329
|
-
*/
|
|
1330
|
-
function generateServerActionFileFromBodies(
|
|
1331
|
-
filePath: string,
|
|
1332
|
-
actions: Array<{
|
|
1333
|
-
name: string;
|
|
1334
|
-
body?: string;
|
|
1335
|
-
description?: string;
|
|
1336
|
-
}>,
|
|
1337
|
-
): string {
|
|
1338
|
-
const lines: string[] = [
|
|
1339
|
-
`/**`,
|
|
1340
|
-
` * Server actions — ${filePath}`,
|
|
1341
|
-
` *`,
|
|
1342
|
-
` * Auto-generated from preserved function bodies.`,
|
|
1343
|
-
` */`,
|
|
1344
|
-
``,
|
|
1345
|
-
`import type { TransitionContext, ActionResult } from '@mindmatrix/react';`,
|
|
1346
|
-
``,
|
|
1347
|
-
];
|
|
1348
|
-
|
|
1349
|
-
for (const action of actions) {
|
|
1350
|
-
if (action.body) {
|
|
1351
|
-
lines.push(action.body);
|
|
1352
|
-
lines.push(``);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
return lines.join('\n');
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
// =============================================================================
|
|
1360
|
-
// Router Transition Reduction
|
|
1361
|
-
// =============================================================================
|
|
1362
|
-
|
|
1363
|
-
/**
|
|
1364
|
-
* Reduces O(n²) router transitions to only meaningful navigation paths:
|
|
1365
|
-
* 1. Intra-role transitions (RIDER_HOME → RIDER_HISTORY — same role prefix)
|
|
1366
|
-
* 2. Cross-role home transitions (RIDER_HOME → DRIVER_DASHBOARD — between "home" states)
|
|
1367
|
-
*
|
|
1368
|
-
* This preserves realistic navigation while removing the every-to-every explosion.
|
|
1369
|
-
*/
|
|
1370
|
-
function reduceRouterTransitions(
|
|
1371
|
-
transitions: IRTransitionDefinition[],
|
|
1372
|
-
states: IRStateDefinition[],
|
|
1373
|
-
): IRTransitionDefinition[] {
|
|
1374
|
-
if (transitions.length === 0) return transitions;
|
|
1375
|
-
|
|
1376
|
-
// Extract role prefixes from state names (e.g., RIDER, DRIVER, ADMIN)
|
|
1377
|
-
const stateNames = new Set(states.map(s => s.name));
|
|
1378
|
-
const roleGroups = new Map<string, string[]>();
|
|
1379
|
-
|
|
1380
|
-
for (const state of states) {
|
|
1381
|
-
const parts = state.name.split('_');
|
|
1382
|
-
// Role prefix is everything before the last segment (e.g., RIDER from RIDER_HOME)
|
|
1383
|
-
// For states like ADMIN_SURGE_PRICING, group by first segment
|
|
1384
|
-
const role = parts[0];
|
|
1385
|
-
if (!roleGroups.has(role)) roleGroups.set(role, []);
|
|
1386
|
-
roleGroups.get(role)!.push(state.name);
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
// Identify "home" states per role (first state in each role group, or ones with HOME/DASHBOARD)
|
|
1390
|
-
const homeStates = new Set<string>();
|
|
1391
|
-
for (const [, group] of roleGroups) {
|
|
1392
|
-
const home = group.find(s =>
|
|
1393
|
-
s.includes('HOME') || s.includes('DASHBOARD') || s.includes('ANALYTICS')
|
|
1394
|
-
) || group[0];
|
|
1395
|
-
homeStates.add(home);
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// Keep transitions that are:
|
|
1399
|
-
// 1. Intra-role (same role prefix)
|
|
1400
|
-
// 2. Between home states of different roles
|
|
1401
|
-
const kept: IRTransitionDefinition[] = [];
|
|
1402
|
-
const seenKeys = new Set<string>();
|
|
1403
|
-
|
|
1404
|
-
for (const t of transitions) {
|
|
1405
|
-
const fromArr = Array.isArray(t.from) ? t.from : [t.from as unknown as string];
|
|
1406
|
-
const fromState = fromArr[0];
|
|
1407
|
-
const toState = t.to;
|
|
1408
|
-
|
|
1409
|
-
if (!fromState || !stateNames.has(fromState) || !stateNames.has(toState)) continue;
|
|
1410
|
-
|
|
1411
|
-
const fromRole = fromState.split('_')[0];
|
|
1412
|
-
const toRole = toState.split('_')[0];
|
|
1413
|
-
const key = `${fromState}→${toState}`;
|
|
1414
|
-
|
|
1415
|
-
if (seenKeys.has(key)) continue;
|
|
1416
|
-
|
|
1417
|
-
// Rule 1: same role group
|
|
1418
|
-
const sameRole = fromRole === toRole;
|
|
1419
|
-
|
|
1420
|
-
// Rule 2: from a home state to another home state
|
|
1421
|
-
const crossRoleHome = homeStates.has(fromState) && homeStates.has(toState);
|
|
1422
|
-
|
|
1423
|
-
if (sameRole || crossRoleHome) {
|
|
1424
|
-
seenKeys.add(key);
|
|
1425
|
-
kept.push(t);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
return kept;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
// =============================================================================
|
|
1433
|
-
// Main: decompileProjectEnhanced
|
|
1434
|
-
// =============================================================================
|
|
1435
|
-
|
|
1436
|
-
/**
|
|
1437
|
-
* Enhanced project decompiler that uses SplitStrategy to determine output
|
|
1438
|
-
* complexity and generates a complete multi-file project.
|
|
1439
|
-
*
|
|
1440
|
-
* @param definition - The full workflow definition to decompile.
|
|
1441
|
-
* @returns Files, entry path, slug, and the split decision used.
|
|
1442
|
-
*/
|
|
1443
|
-
export function decompileProjectEnhanced(
|
|
1444
|
-
definition: DecompilerInput,
|
|
1445
|
-
): EnhancedDecompileResult {
|
|
1446
|
-
const slug = definition.slug;
|
|
1447
|
-
const decision = determineSplitStrategy(definition);
|
|
1448
|
-
const files: EnhancedProjectFile[] = [];
|
|
1449
|
-
const modelPaths: string[] = [];
|
|
1450
|
-
const entryPaths: string[] = [];
|
|
1451
|
-
const actionPaths: string[] = [];
|
|
1452
|
-
|
|
1453
|
-
// --- Single file mode ---
|
|
1454
|
-
if (decision.tier === 'single') {
|
|
1455
|
-
const result = decompile(definition);
|
|
1456
|
-
files.push({
|
|
1457
|
-
path: 'main.workflow.tsx',
|
|
1458
|
-
role: 'view-entry',
|
|
1459
|
-
content: result.code,
|
|
1460
|
-
});
|
|
1461
|
-
// Even in single-file mode, emit components and actions if present in metadata
|
|
1462
|
-
emitComponentDefinitions(definition, files);
|
|
1463
|
-
emitServerActionsFromMetadata(definition, files);
|
|
1464
|
-
return { files, entryFile: 'main.workflow.tsx', slug, splitDecision: decision };
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
// --- Model file ---
|
|
1468
|
-
if (decision.emitModels && definition.fields.length > 0) {
|
|
1469
|
-
const modelPath = `models/${slug}.ts`;
|
|
1470
|
-
const modelContent = generateModelFile(
|
|
1471
|
-
slug,
|
|
1472
|
-
definition.fields,
|
|
1473
|
-
definition.states,
|
|
1474
|
-
definition.transitions,
|
|
1475
|
-
{ version: definition.version, category: Array.isArray(definition.category) ? definition.category[0] : definition.category, description: definition.description },
|
|
1476
|
-
);
|
|
1477
|
-
files.push({ path: modelPath, role: 'model', content: modelContent });
|
|
1478
|
-
modelPaths.push(modelPath);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
// --- Server actions ---
|
|
1482
|
-
if (decision.emitActions) {
|
|
1483
|
-
const serverActions = extractServerActions(definition.states, definition.transitions);
|
|
1484
|
-
if (serverActions.length > 0) {
|
|
1485
|
-
const actionPath = `actions/${slug}.server.ts`;
|
|
1486
|
-
const actionContent = generateServerActionFile(slug, serverActions);
|
|
1487
|
-
files.push({ path: actionPath, role: 'server-action', content: actionContent });
|
|
1488
|
-
actionPaths.push(actionPath);
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
// --- Pages from experience tree (role-based split) ---
|
|
1493
|
-
let pagesExtracted = false;
|
|
1494
|
-
let extractedPages: PageSection[] = [];
|
|
1495
|
-
let extractedChildViews: IRExperienceNode[] = [];
|
|
1496
|
-
// Track whether pages were extracted from an existing Router/Route structure.
|
|
1497
|
-
// If so, the router child definition is compiler-generated and should be skipped.
|
|
1498
|
-
let routerIsCompilerGenerated = false;
|
|
1499
|
-
if (decision.emitPages && definition.experience) {
|
|
1500
|
-
// Check if Router/Route structure exists (indicates compiler-generated router)
|
|
1501
|
-
const routerNode = definition.experience.children?.find(
|
|
1502
|
-
(c: IRExperienceNode) => c.component === 'Router',
|
|
1503
|
-
);
|
|
1504
|
-
routerIsCompilerGenerated = !!routerNode;
|
|
1505
|
-
|
|
1506
|
-
const { pages, childViews } = extractPageSections(
|
|
1507
|
-
definition.experience,
|
|
1508
|
-
definition.childDefinitions,
|
|
1509
|
-
);
|
|
1510
|
-
if (pages.length > 0) {
|
|
1511
|
-
pagesExtracted = true;
|
|
1512
|
-
extractedPages = pages;
|
|
1513
|
-
extractedChildViews = childViews;
|
|
1514
|
-
|
|
1515
|
-
// Build slug → model path map for type-safe imports
|
|
1516
|
-
const modelSlugToPath = new Map<string, string>();
|
|
1517
|
-
modelSlugToPath.set(slug, `models/${slug}.ts`);
|
|
1518
|
-
if (definition.childDefinitions) {
|
|
1519
|
-
for (const child of definition.childDefinitions) {
|
|
1520
|
-
const cs = child.slug;
|
|
1521
|
-
if (!cs.endsWith('-router') && child.category !== 'router') {
|
|
1522
|
-
modelSlugToPath.set(cs, `models/${cs}.ts`);
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
for (const page of pages) {
|
|
1528
|
-
const pageContent = generatePageFileFromSection(page, modelSlugToPath);
|
|
1529
|
-
files.push({ path: page.filePath, role: 'page', content: pageContent });
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// --- Layout with auth guards ---
|
|
1535
|
-
if (definition.roles && definition.roles.length > 0 && decision.tier === 'large') {
|
|
1536
|
-
const layoutContent = generateLayoutFile(slug, definition.roles);
|
|
1537
|
-
files.push({ path: 'app/layout.tsx', role: 'layout', content: layoutContent });
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
// --- Child definitions (blueprint support) ---
|
|
1541
|
-
// Merge child definitions by slug (compiler may produce duplicates when
|
|
1542
|
-
// both .workflow.tsx and models/*.ts share the same slug). Merge fields,
|
|
1543
|
-
// states, transitions — required=true wins, non-empty default_value wins.
|
|
1544
|
-
// Also skip: router definitions generated from page files, and children whose
|
|
1545
|
-
// slug matches the parent (they are the parent, not a child).
|
|
1546
|
-
const emittedModelSlugs = new Set(modelPaths.map(p => p.replace('models/', '').replace('.ts', '')));
|
|
1547
|
-
const mergedChildren = new Map<string, DecompilerInput>();
|
|
1548
|
-
if (definition.childDefinitions && definition.childDefinitions.length > 0) {
|
|
1549
|
-
for (const child of definition.childDefinitions) {
|
|
1550
|
-
const childSlug = child.slug;
|
|
1551
|
-
if (childSlug === slug) continue; // Skip parent-slug children
|
|
1552
|
-
const isRouter = childSlug.endsWith('-router') || child.category === 'router';
|
|
1553
|
-
if (isRouter && routerIsCompilerGenerated) continue;
|
|
1554
|
-
|
|
1555
|
-
if (!mergedChildren.has(childSlug)) {
|
|
1556
|
-
mergedChildren.set(childSlug, { ...child });
|
|
1557
|
-
} else {
|
|
1558
|
-
// Merge: fields (required + default_value wins), states, transitions
|
|
1559
|
-
const existing = mergedChildren.get(childSlug)!;
|
|
1560
|
-
// Merge fields: merge metadata for existing fields, add new ones
|
|
1561
|
-
const fieldMap = new Map(existing.fields.map(f => [f.name, f]));
|
|
1562
|
-
for (const f of child.fields) {
|
|
1563
|
-
if (fieldMap.has(f.name)) {
|
|
1564
|
-
const ef = fieldMap.get(f.name)!;
|
|
1565
|
-
if (f.required && !ef.required) ef.required = true;
|
|
1566
|
-
if (f.default_value != null && (ef.default_value == null || ef.default_value === '' || ef.default_value === 0 || ef.default_value === false)) {
|
|
1567
|
-
ef.default_value = f.default_value;
|
|
1568
|
-
}
|
|
1569
|
-
} else {
|
|
1570
|
-
existing.fields.push(f);
|
|
1571
|
-
fieldMap.set(f.name, f);
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
// Merge states: if a later child has MORE states, prefer its complete set
|
|
1575
|
-
// (model IRs have explicit states arrays, workflow IRs only have inferred states)
|
|
1576
|
-
if (child.states.length > existing.states.length) {
|
|
1577
|
-
// Later child has more states — use its set as the base
|
|
1578
|
-
const existingByName = new Map(existing.states.map(s => [s.name, s]));
|
|
1579
|
-
existing.states = child.states.map(s => {
|
|
1580
|
-
// Preserve actions from existing state if any
|
|
1581
|
-
const ex = existingByName.get(s.name);
|
|
1582
|
-
if (ex && (ex.on_enter.length || ex.on_exit.length || ex.during.length)) {
|
|
1583
|
-
return { ...s, on_enter: ex.on_enter, on_exit: ex.on_exit, during: ex.during };
|
|
1584
|
-
}
|
|
1585
|
-
return s;
|
|
1586
|
-
});
|
|
1587
|
-
// Add non-default states from existing that aren't in the child.
|
|
1588
|
-
// Skip 'draft' if the child already has a START state (it's a compiler default).
|
|
1589
|
-
const childNames = new Set(child.states.map(s => s.name));
|
|
1590
|
-
const hasStart = child.states.some(s => s.type === 'START');
|
|
1591
|
-
for (const s of existingByName.values()) {
|
|
1592
|
-
if (!childNames.has(s.name)) {
|
|
1593
|
-
if (s.name === 'draft' && s.type === 'START' && hasStart) continue;
|
|
1594
|
-
existing.states.push(s);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
} else {
|
|
1598
|
-
// Just add missing states
|
|
1599
|
-
const stateNames = new Set(existing.states.map(s => s.name));
|
|
1600
|
-
for (const s of child.states) {
|
|
1601
|
-
if (!stateNames.has(s.name)) {
|
|
1602
|
-
existing.states.push(s);
|
|
1603
|
-
stateNames.add(s.name);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
// Merge transitions (add missing)
|
|
1608
|
-
const transNames = new Set(existing.transitions.map(t => t.name));
|
|
1609
|
-
for (const t of child.transitions) {
|
|
1610
|
-
if (!transNames.has(t.name)) {
|
|
1611
|
-
existing.transitions.push(t);
|
|
1612
|
-
transNames.add(t.name);
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
// Prefer non-default version/category/description
|
|
1616
|
-
if (child.version && child.version !== '0.1.0' && (!existing.version || existing.version === '0.1.0')) {
|
|
1617
|
-
existing.version = child.version;
|
|
1618
|
-
}
|
|
1619
|
-
if (child.category && child.category !== 'data' && (!existing.category || existing.category === 'data')) {
|
|
1620
|
-
existing.category = child.category;
|
|
1621
|
-
}
|
|
1622
|
-
if (child.description && !existing.description) {
|
|
1623
|
-
existing.description = child.description;
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
for (const [childSlug, child] of mergedChildren) {
|
|
1630
|
-
const isRouter = childSlug.endsWith('-router') || child.category === 'router';
|
|
1631
|
-
|
|
1632
|
-
// For routers, reduce O(n²) transitions to adjacent/related routes
|
|
1633
|
-
const childTransitions = isRouter
|
|
1634
|
-
? reduceRouterTransitions(child.transitions, child.states)
|
|
1635
|
-
: child.transitions;
|
|
1636
|
-
|
|
1637
|
-
// Child model — skip if parent already emitted a model for this slug
|
|
1638
|
-
if (child.fields.length > 0 && !emittedModelSlugs.has(childSlug)) {
|
|
1639
|
-
const childModelPath = `models/${childSlug}.ts`;
|
|
1640
|
-
files.push({
|
|
1641
|
-
path: childModelPath,
|
|
1642
|
-
role: 'model',
|
|
1643
|
-
content: generateModelFile(childSlug, child.fields, child.states, childTransitions,
|
|
1644
|
-
{ version: child.version, category: Array.isArray(child.category) ? child.category[0] : child.category, description: child.description }),
|
|
1645
|
-
});
|
|
1646
|
-
modelPaths.push(childModelPath);
|
|
1647
|
-
emittedModelSlugs.add(childSlug);
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
// Child workflow file — preserve parent version/category if child uses defaults
|
|
1651
|
-
const childInput: DecompilerInput = {
|
|
1652
|
-
...child,
|
|
1653
|
-
version: child.version || definition.version,
|
|
1654
|
-
category: child.category || definition.category,
|
|
1655
|
-
transitions: childTransitions,
|
|
1656
|
-
experience: ((child as unknown as Record<string, unknown>).views as Record<string, unknown> | undefined)?.default as IRExperienceNode | undefined,
|
|
1657
|
-
};
|
|
1658
|
-
const childResult = decompile(childInput, {
|
|
1659
|
-
componentName: pascalCase(childSlug),
|
|
1660
|
-
includeAnnotation: true,
|
|
1661
|
-
});
|
|
1662
|
-
const childPath = `${childSlug}.workflow.tsx`;
|
|
1663
|
-
files.push({ path: childPath, role: 'view-entry', content: childResult.code });
|
|
1664
|
-
entryPaths.push(childPath);
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// --- Component definitions from metadata ---
|
|
1668
|
-
emitComponentDefinitions(definition, files);
|
|
1669
|
-
|
|
1670
|
-
// --- Server actions with preserved bodies from metadata ---
|
|
1671
|
-
const emittedActionPaths = emitServerActionsFromMetadata(definition, files);
|
|
1672
|
-
actionPaths.push(...emittedActionPaths);
|
|
1673
|
-
|
|
1674
|
-
// --- Main entry file ---
|
|
1675
|
-
const mainPath = 'main.workflow.tsx';
|
|
1676
|
-
if (pagesExtracted) {
|
|
1677
|
-
const mainContent = generateMainWithPages(definition, extractedPages, extractedChildViews);
|
|
1678
|
-
files.push({ path: mainPath, role: 'view-entry', content: mainContent });
|
|
1679
|
-
} else {
|
|
1680
|
-
const mainResult = decompile(definition);
|
|
1681
|
-
files.push({ path: mainPath, role: 'view-entry', content: mainResult.code });
|
|
1682
|
-
}
|
|
1683
|
-
entryPaths.push(mainPath);
|
|
1684
|
-
|
|
1685
|
-
// --- mm.config.ts ---
|
|
1686
|
-
if (decision.emitConfig) {
|
|
1687
|
-
// Deduplicate paths before config generation (second pass may add duplicates)
|
|
1688
|
-
const configData = extractConfigData(
|
|
1689
|
-
definition,
|
|
1690
|
-
[...new Set(modelPaths)],
|
|
1691
|
-
[...new Set(entryPaths)],
|
|
1692
|
-
[...new Set(actionPaths)],
|
|
1693
|
-
);
|
|
1694
|
-
const configContent = generateMmConfig(configData);
|
|
1695
|
-
files.push({ path: 'mm.config.ts', role: 'config', content: configContent });
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
// --- package.json ---
|
|
1699
|
-
files.push({
|
|
1700
|
-
path: 'package.json',
|
|
1701
|
-
role: 'config',
|
|
1702
|
-
content: generatePackageJson(definition),
|
|
1703
|
-
});
|
|
1704
|
-
|
|
1705
|
-
// --- tsconfig.json ---
|
|
1706
|
-
files.push({
|
|
1707
|
-
path: 'tsconfig.json',
|
|
1708
|
-
role: 'config',
|
|
1709
|
-
content: TSCONFIG_TEMPLATE,
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
return {
|
|
1713
|
-
files,
|
|
1714
|
-
entryFile: mainPath,
|
|
1715
|
-
slug,
|
|
1716
|
-
splitDecision: decision,
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
// =============================================================================
|
|
1721
|
-
// Package scaffolding
|
|
1722
|
-
// =============================================================================
|
|
1723
|
-
|
|
1724
|
-
const TSCONFIG_TEMPLATE = JSON.stringify({
|
|
1725
|
-
compilerOptions: {
|
|
1726
|
-
target: 'ES2022',
|
|
1727
|
-
module: 'ESNext',
|
|
1728
|
-
moduleResolution: 'bundler',
|
|
1729
|
-
jsx: 'react-jsx',
|
|
1730
|
-
strict: true,
|
|
1731
|
-
esModuleInterop: true,
|
|
1732
|
-
skipLibCheck: true,
|
|
1733
|
-
forceConsistentCasingInFileNames: true,
|
|
1734
|
-
declaration: true,
|
|
1735
|
-
sourceMap: true,
|
|
1736
|
-
outDir: 'dist',
|
|
1737
|
-
rootDir: '.',
|
|
1738
|
-
baseUrl: '.',
|
|
1739
|
-
paths: { '@/*': ['./*'] },
|
|
1740
|
-
},
|
|
1741
|
-
include: ['**/*.ts', '**/*.tsx'],
|
|
1742
|
-
exclude: ['node_modules', 'dist'],
|
|
1743
|
-
}, null, 2) + '\n';
|
|
1744
|
-
|
|
1745
|
-
function generatePackageJson(def: DecompilerInput): string {
|
|
1746
|
-
const pkg = {
|
|
1747
|
-
name: `@mindmatrix/blueprint-${def.slug}`,
|
|
1748
|
-
version: def.version || '1.0.0',
|
|
1749
|
-
description: def.description || '',
|
|
1750
|
-
type: 'module',
|
|
1751
|
-
main: 'main.workflow.tsx',
|
|
1752
|
-
scripts: {
|
|
1753
|
-
'type-check': 'tsc --noEmit',
|
|
1754
|
-
build: 'mmrc build --src .',
|
|
1755
|
-
deploy: 'mmrc deploy --build --src .',
|
|
1756
|
-
},
|
|
1757
|
-
peerDependencies: {
|
|
1758
|
-
react: '>=18.0.0',
|
|
1759
|
-
'@mindmatrix/react': 'workspace:*',
|
|
1760
|
-
},
|
|
1761
|
-
devDependencies: {
|
|
1762
|
-
'@mindmatrix/react-compiler': 'workspace:*',
|
|
1763
|
-
'@types/react': '^19.0.0',
|
|
1764
|
-
typescript: '^5.4.0',
|
|
1765
|
-
},
|
|
1766
|
-
};
|
|
1767
|
-
return JSON.stringify(pkg, null, 2) + '\n';
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
// =============================================================================
|
|
1771
|
-
// Helpers
|
|
1772
|
-
// =============================================================================
|
|
1773
|
-
|
|
1774
|
-
function esc(s: string): string {
|
|
1775
|
-
return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
1776
|
-
}
|