@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,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { transform } from '../index';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Round-trip invariant: forward(source) → reverse → ≈ source
|
|
6
|
-
*
|
|
7
|
-
* "≈" means semantically equivalent — whitespace may differ,
|
|
8
|
-
* and plain divs may gain flex/flex-col (since atoms default to Stack).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
function roundTrip(source: string): string {
|
|
12
|
-
const forwarded = transform(source, { direction: 'forward', annotate: true });
|
|
13
|
-
const reversed = transform(forwarded.code, { direction: 'reverse', annotate: false });
|
|
14
|
-
return reversed.code;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalize(s: string): string {
|
|
18
|
-
return s.replace(/\s+/g, ' ').trim();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('round-trip invariant', () => {
|
|
22
|
-
it('preserves a simple flex row', () => {
|
|
23
|
-
const input = `const App = () => <div className="flex items-center gap-2">hello</div>;`;
|
|
24
|
-
const result = roundTrip(input);
|
|
25
|
-
expect(normalize(result)).toContain('flex items-center gap-2');
|
|
26
|
-
expect(result).toContain('<div');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('preserves a flex-col stack', () => {
|
|
30
|
-
const input = `const App = () => <div className="flex flex-col gap-4 p-2">hello</div>;`;
|
|
31
|
-
const result = roundTrip(input);
|
|
32
|
-
expect(normalize(result)).toContain('flex flex-col gap-4 p-2');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('preserves conditional rendering', () => {
|
|
36
|
-
const input = `const App = () => <div className="flex">{visible && <span className="text-sm">hi</span>}</div>;`;
|
|
37
|
-
const result = roundTrip(input);
|
|
38
|
-
expect(result).toContain('visible &&');
|
|
39
|
-
expect(result).toContain('<span');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('preserves .map() rendering', () => {
|
|
43
|
-
const input = `const App = () => (
|
|
44
|
-
<div className="flex flex-col">
|
|
45
|
-
{items.map(item => <span key={item.id}>{item.name}</span>)}
|
|
46
|
-
</div>
|
|
47
|
-
);`;
|
|
48
|
-
const result = roundTrip(input);
|
|
49
|
-
expect(result).toContain('.map(');
|
|
50
|
-
expect(result).toContain('<span');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('preserves button with onClick', () => {
|
|
54
|
-
const input = `const App = () => <button className="px-4 py-2" onClick={handleClick}>Go</button>;`;
|
|
55
|
-
const result = roundTrip(input);
|
|
56
|
-
expect(result).toContain('<button');
|
|
57
|
-
expect(result).toContain('onClick');
|
|
58
|
-
expect(result).toContain('px-4 py-2');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('preserves img', () => {
|
|
62
|
-
const input = `const App = () => <img src="/avatar.png" className="w-9 h-9 rounded-full" />;`;
|
|
63
|
-
const result = roundTrip(input);
|
|
64
|
-
expect(result).toContain('<img');
|
|
65
|
-
expect(result).toContain('w-9 h-9 rounded-full');
|
|
66
|
-
expect(result).toContain('src="/avatar.png"');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('preserves headings', () => {
|
|
70
|
-
const input = `const App = () => <h2 className="font-bold text-lg">Title</h2>;`;
|
|
71
|
-
const result = roundTrip(input);
|
|
72
|
-
expect(result).toContain('<h2');
|
|
73
|
-
expect(result).toContain('font-bold');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('preserves textarea', () => {
|
|
77
|
-
const input = `const App = () => <textarea className="w-full h-20" />;`;
|
|
78
|
-
const result = roundTrip(input);
|
|
79
|
-
expect(result).toContain('<textarea');
|
|
80
|
-
expect(result).toContain('w-full h-20');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('preserves a full component with mixed patterns', () => {
|
|
84
|
-
const input = `
|
|
85
|
-
import React, { useState } from 'react';
|
|
86
|
-
import { Hash } from 'lucide-react';
|
|
87
|
-
|
|
88
|
-
const ChannelItem = ({ channel, isActive, onSelect }) => {
|
|
89
|
-
const [hovering, setHovering] = useState(false);
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<div
|
|
93
|
-
className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-gray-800 cursor-pointer"
|
|
94
|
-
onClick={() => onSelect(channel.id)}
|
|
95
|
-
onMouseEnter={() => setHovering(true)}
|
|
96
|
-
onMouseLeave={() => setHovering(false)}
|
|
97
|
-
>
|
|
98
|
-
<Hash className="w-3.5 h-3.5 text-gray-400 shrink-0" />
|
|
99
|
-
<span className="text-sm text-gray-300 truncate flex-1">
|
|
100
|
-
{channel.name}
|
|
101
|
-
</span>
|
|
102
|
-
{channel.unreadCount > 0 && (
|
|
103
|
-
<span className="bg-blue-500 text-white text-xs px-1.5 rounded-full min-w-[18px] text-center">
|
|
104
|
-
{channel.unreadCount}
|
|
105
|
-
</span>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
};`;
|
|
110
|
-
const result = roundTrip(input);
|
|
111
|
-
|
|
112
|
-
// All key features preserved
|
|
113
|
-
expect(result).toContain('useState');
|
|
114
|
-
expect(result).toContain('Hash');
|
|
115
|
-
expect(result).toContain('channel.name');
|
|
116
|
-
expect(result).toContain('channel.unreadCount');
|
|
117
|
-
expect(result).toContain('hover:bg-gray-800');
|
|
118
|
-
expect(result).toContain('cursor-pointer');
|
|
119
|
-
expect(result).toContain('lucide-react');
|
|
120
|
-
expect(result).toContain('onMouseEnter');
|
|
121
|
-
expect(result).toContain('onMouseLeave');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('preserves motion.div (not transformed)', () => {
|
|
125
|
-
const input = `const App = () => <motion.div className="flex" initial={{ opacity: 0 }}>hi</motion.div>;`;
|
|
126
|
-
const result = roundTrip(input);
|
|
127
|
-
expect(result).toContain('motion.div');
|
|
128
|
-
expect(result).toContain('initial=');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('preserves PascalCase components (not transformed)', () => {
|
|
132
|
-
const input = `const App = () => <AnimatePresence><MyWidget>hi</MyWidget></AnimatePresence>;`;
|
|
133
|
-
const result = roundTrip(input);
|
|
134
|
-
expect(result).toContain('<AnimatePresence');
|
|
135
|
-
expect(result).toContain('<MyWidget');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Round-trip annotation — preserves original element info as JSDoc comments.
|
|
3
|
-
*
|
|
4
|
-
* Format: @mm-original tagName.class1.class2.class3
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as t from '@babel/types';
|
|
8
|
-
|
|
9
|
-
const ANNOTATION_PREFIX = '@mm-original';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Create an annotation comment for a transformed element.
|
|
13
|
-
*/
|
|
14
|
-
export function createAnnotation(
|
|
15
|
-
originalTag: string,
|
|
16
|
-
classNames: string[],
|
|
17
|
-
): string {
|
|
18
|
-
const parts = [originalTag, ...classNames.slice(0, 10)]; // limit to 10 classes
|
|
19
|
-
return `${ANNOTATION_PREFIX} ${parts.join('.')}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Parse an annotation comment.
|
|
24
|
-
* Returns { tag, classes } or null if not an annotation.
|
|
25
|
-
*/
|
|
26
|
-
export function parseAnnotation(
|
|
27
|
-
comment: string,
|
|
28
|
-
): { tag: string; classes: string[] } | null {
|
|
29
|
-
const trimmed = comment.trim();
|
|
30
|
-
if (!trimmed.startsWith(ANNOTATION_PREFIX)) return null;
|
|
31
|
-
|
|
32
|
-
const rest = trimmed.slice(ANNOTATION_PREFIX.length).trim();
|
|
33
|
-
if (!rest) return null;
|
|
34
|
-
|
|
35
|
-
const parts = rest.split('.');
|
|
36
|
-
const tag = parts[0];
|
|
37
|
-
const classes = parts.slice(1);
|
|
38
|
-
|
|
39
|
-
return { tag, classes };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Add a leading comment annotation to a JSX element node.
|
|
44
|
-
*/
|
|
45
|
-
export function addAnnotationComment(
|
|
46
|
-
node: t.Node,
|
|
47
|
-
originalTag: string,
|
|
48
|
-
classNames: string[],
|
|
49
|
-
): void {
|
|
50
|
-
const text = ` ${createAnnotation(originalTag, classNames)} `;
|
|
51
|
-
if (!node.leadingComments) {
|
|
52
|
-
node.leadingComments = [];
|
|
53
|
-
}
|
|
54
|
-
node.leadingComments.push({
|
|
55
|
-
type: 'CommentBlock',
|
|
56
|
-
value: text,
|
|
57
|
-
} as t.Comment);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Read and remove the annotation comment from a node.
|
|
62
|
-
* Returns parsed annotation or null.
|
|
63
|
-
*/
|
|
64
|
-
export function readAnnotationComment(
|
|
65
|
-
node: t.Node,
|
|
66
|
-
): { tag: string; classes: string[] } | null {
|
|
67
|
-
if (!node.leadingComments) return null;
|
|
68
|
-
|
|
69
|
-
for (let i = node.leadingComments.length - 1; i >= 0; i--) {
|
|
70
|
-
const comment = node.leadingComments[i];
|
|
71
|
-
const parsed = parseAnnotation(comment.value);
|
|
72
|
-
if (parsed) {
|
|
73
|
-
// Remove the annotation comment
|
|
74
|
-
node.leadingComments.splice(i, 1);
|
|
75
|
-
return parsed;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Strip all @mm-original annotations from a node and its children.
|
|
84
|
-
* Used with --strip-annotations flag.
|
|
85
|
-
*/
|
|
86
|
-
export function stripAnnotations(node: t.Node): void {
|
|
87
|
-
if (node.leadingComments) {
|
|
88
|
-
node.leadingComments = node.leadingComments.filter(
|
|
89
|
-
c => !parseAnnotation(c.value),
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
if (node.trailingComments) {
|
|
93
|
-
node.trailingComments = node.trailingComments.filter(
|
|
94
|
-
c => !parseAnnotation(c.value),
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
}
|
package/src/codemod/classify.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Element classifier — analyzes tag name + className to determine target atom.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type * as t from '@babel/types';
|
|
6
|
-
import { FORWARD_RULES, REVERSE_RULES, CONTROL_FLOW_ATOMS, TEXT_VARIANT_TO_TAG } from './rules';
|
|
7
|
-
import type { MappingRule } from './rules';
|
|
8
|
-
|
|
9
|
-
export interface ClassifyResult {
|
|
10
|
-
atom: string;
|
|
11
|
-
/** Classes to remove from className (implicit in atom) */
|
|
12
|
-
removeClasses: string[];
|
|
13
|
-
/** Props to add to the element */
|
|
14
|
-
addProps: Record<string, string | boolean>;
|
|
15
|
-
/** Original tag for annotation */
|
|
16
|
-
originalTag: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ReverseClassifyResult {
|
|
20
|
-
htmlTag: string;
|
|
21
|
-
/** Classes to inject into className */
|
|
22
|
-
injectClasses: string[];
|
|
23
|
-
/** Props to remove (encoded in HTML tag) */
|
|
24
|
-
removeProps: string[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Extract static class names from a className JSX attribute value.
|
|
29
|
-
* Handles: string literals, template literals (static parts), cn() first arg.
|
|
30
|
-
*/
|
|
31
|
-
export function extractStaticClasses(attrValue: t.Node | null | undefined): string[] {
|
|
32
|
-
if (!attrValue) return [];
|
|
33
|
-
|
|
34
|
-
// className="flex items-center"
|
|
35
|
-
if (attrValue.type === 'StringLiteral') {
|
|
36
|
-
return attrValue.value.split(/\s+/).filter(Boolean);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// className={expr} — unwrap JSXExpressionContainer
|
|
40
|
-
if (attrValue.type === 'JSXExpressionContainer') {
|
|
41
|
-
return extractStaticClasses(attrValue.expression as t.Node);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// className={`flex items-center ${dynamic}`}
|
|
45
|
-
if (attrValue.type === 'TemplateLiteral') {
|
|
46
|
-
const staticParts = attrValue.quasis.map(q => q.value.raw).join(' ');
|
|
47
|
-
return staticParts.split(/\s+/).filter(Boolean);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// className={cn("flex items-center", dynamic)}
|
|
51
|
-
if (
|
|
52
|
-
attrValue.type === 'CallExpression' &&
|
|
53
|
-
attrValue.callee.type === 'Identifier' &&
|
|
54
|
-
attrValue.callee.name === 'cn' &&
|
|
55
|
-
attrValue.arguments.length > 0
|
|
56
|
-
) {
|
|
57
|
-
const firstArg = attrValue.arguments[0];
|
|
58
|
-
if (firstArg.type === 'StringLiteral') {
|
|
59
|
-
return firstArg.value.split(/\s+/).filter(Boolean);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return [];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Classify a raw HTML element into its target workflow atom.
|
|
68
|
-
*/
|
|
69
|
-
export function classifyElement(
|
|
70
|
-
tagName: string,
|
|
71
|
-
classNames: string[],
|
|
72
|
-
): ClassifyResult | null {
|
|
73
|
-
// Skip elements that are already atoms or components (PascalCase)
|
|
74
|
-
if (tagName[0] === tagName[0].toUpperCase() && tagName[0] !== tagName[0].toLowerCase()) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Skip motion.* elements
|
|
79
|
-
if (tagName.startsWith('motion.')) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Find matching rules for this tag, sorted by priority descending
|
|
84
|
-
const candidates = FORWARD_RULES
|
|
85
|
-
.filter(r => r.htmlTag === tagName)
|
|
86
|
-
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
87
|
-
|
|
88
|
-
if (candidates.length === 0) return null;
|
|
89
|
-
|
|
90
|
-
// For div, we need to check className signals
|
|
91
|
-
if (tagName === 'div') {
|
|
92
|
-
for (const rule of candidates) {
|
|
93
|
-
if (!rule.classSignal || rule.classSignal.length === 0) {
|
|
94
|
-
// Default rule (no signal required) — only use if no better match
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
const hasAllSignals = rule.classSignal.every(s => classNames.includes(s));
|
|
98
|
-
if (hasAllSignals) {
|
|
99
|
-
return makeResult(rule, tagName);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Fallback: default div rule (priority 0)
|
|
103
|
-
const defaultRule = candidates.find(r => (r.priority ?? 0) === 0);
|
|
104
|
-
if (defaultRule) {
|
|
105
|
-
return makeResult(defaultRule, tagName);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// For non-div elements, take the first (and usually only) match
|
|
110
|
-
const rule = candidates[0];
|
|
111
|
-
return makeResult(rule, tagName);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function makeResult(rule: MappingRule, tagName: string): ClassifyResult {
|
|
115
|
-
return {
|
|
116
|
-
atom: rule.atom,
|
|
117
|
-
removeClasses: rule.implicitClasses ?? [],
|
|
118
|
-
addProps: rule.props ?? {},
|
|
119
|
-
originalTag: tagName,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Reverse-classify a workflow atom into its target HTML element.
|
|
125
|
-
*/
|
|
126
|
-
export function reverseClassifyAtom(
|
|
127
|
-
atomName: string,
|
|
128
|
-
props: Map<string, t.Node>,
|
|
129
|
-
): ReverseClassifyResult | null {
|
|
130
|
-
// Skip control flow atoms
|
|
131
|
-
if (CONTROL_FLOW_ATOMS.has(atomName)) return null;
|
|
132
|
-
|
|
133
|
-
// Special case: Text with variant → heading tag
|
|
134
|
-
if (atomName === 'Text') {
|
|
135
|
-
const variantNode = props.get('variant');
|
|
136
|
-
if (variantNode && variantNode.type === 'StringLiteral') {
|
|
137
|
-
const tag = TEXT_VARIANT_TO_TAG[variantNode.value];
|
|
138
|
-
if (tag) {
|
|
139
|
-
return {
|
|
140
|
-
htmlTag: tag,
|
|
141
|
-
injectClasses: [],
|
|
142
|
-
removeProps: ['variant'],
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// Default Text → span
|
|
147
|
-
return { htmlTag: 'span', injectClasses: [], removeProps: [] };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Special case: TextInput with multiline → textarea
|
|
151
|
-
if (atomName === 'TextInput') {
|
|
152
|
-
const multilineNode = props.get('multiline');
|
|
153
|
-
if (multilineNode) {
|
|
154
|
-
const isMultiline =
|
|
155
|
-
(multilineNode.type === 'BooleanLiteral' && multilineNode.value) ||
|
|
156
|
-
(multilineNode.type === 'JSXExpressionContainer' &&
|
|
157
|
-
(multilineNode as any).expression?.type === 'BooleanLiteral' &&
|
|
158
|
-
(multilineNode as any).expression?.value);
|
|
159
|
-
if (isMultiline) {
|
|
160
|
-
return { htmlTag: 'textarea', injectClasses: [], removeProps: ['multiline'] };
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return { htmlTag: 'input', injectClasses: [], removeProps: [] };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const rule = REVERSE_RULES.find(r => r.atom === atomName);
|
|
167
|
-
if (!rule) return null;
|
|
168
|
-
|
|
169
|
-
return {
|
|
170
|
-
htmlTag: rule.htmlTag,
|
|
171
|
-
injectClasses: rule.injectClasses ?? [],
|
|
172
|
-
removeProps: rule.removeProps ?? [],
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Remove classes from a className string.
|
|
178
|
-
*/
|
|
179
|
-
export function removeClassesFromString(className: string, toRemove: string[]): string {
|
|
180
|
-
if (toRemove.length === 0) return className;
|
|
181
|
-
const removeSet = new Set(toRemove);
|
|
182
|
-
return className
|
|
183
|
-
.split(/\s+/)
|
|
184
|
-
.filter(c => c && !removeSet.has(c))
|
|
185
|
-
.join(' ');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Add classes to the beginning of a className string.
|
|
190
|
-
*/
|
|
191
|
-
export function addClassesToString(className: string, toAdd: string[]): string {
|
|
192
|
-
if (toAdd.length === 0) return className;
|
|
193
|
-
const existing = new Set(className.split(/\s+/).filter(Boolean));
|
|
194
|
-
const newClasses = toAdd.filter(c => !existing.has(c));
|
|
195
|
-
if (newClasses.length === 0) return className;
|
|
196
|
-
return [...newClasses, ...className.split(/\s+/).filter(Boolean)].join(' ');
|
|
197
|
-
}
|
package/src/codemod/cli.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CLI for the bidirectional React ↔ Workflow Atom codemod.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* npx mm-codemod forward src/features/chat/
|
|
7
|
-
* npx mm-codemod reverse packages/blueprint-chat/app/
|
|
8
|
-
* npx mm-codemod verify src/features/chat/
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
12
|
-
import { resolve, relative, dirname } from 'path';
|
|
13
|
-
import { glob } from 'glob';
|
|
14
|
-
import { transform, type CodemodOptions } from './index';
|
|
15
|
-
import type { StateMode } from './state-transform';
|
|
16
|
-
|
|
17
|
-
interface CLIOptions {
|
|
18
|
-
direction: 'forward' | 'reverse' | 'verify';
|
|
19
|
-
paths: string[];
|
|
20
|
-
dryRun: boolean;
|
|
21
|
-
stripAnnotations: boolean;
|
|
22
|
-
stateMode: StateMode;
|
|
23
|
-
verbose: boolean;
|
|
24
|
-
outDir?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function parseArgs(argv: string[]): CLIOptions {
|
|
28
|
-
const args = argv.slice(2);
|
|
29
|
-
const direction = args[0] as CLIOptions['direction'];
|
|
30
|
-
|
|
31
|
-
if (!direction || !['forward', 'reverse', 'verify'].includes(direction)) {
|
|
32
|
-
console.error('Usage: mm-codemod <forward|reverse|verify> <paths...> [options]');
|
|
33
|
-
console.error('');
|
|
34
|
-
console.error('Options:');
|
|
35
|
-
console.error(' --dry-run Print diff without writing');
|
|
36
|
-
console.error(' --strip-annotations Remove @mm-original comments');
|
|
37
|
-
console.error(' --mode <mode> State mode: preserve|migrate|annotate');
|
|
38
|
-
console.error(' --out <dir> Output directory (default: in-place)');
|
|
39
|
-
console.error(' --verbose Log every transformation');
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const paths: string[] = [];
|
|
44
|
-
let dryRun = false;
|
|
45
|
-
let stripAnnotations = false;
|
|
46
|
-
let stateMode: StateMode = 'preserve';
|
|
47
|
-
let verbose = false;
|
|
48
|
-
let outDir: string | undefined;
|
|
49
|
-
|
|
50
|
-
for (let i = 1; i < args.length; i++) {
|
|
51
|
-
const arg = args[i];
|
|
52
|
-
if (arg === '--dry-run') {
|
|
53
|
-
dryRun = true;
|
|
54
|
-
} else if (arg === '--strip-annotations') {
|
|
55
|
-
stripAnnotations = true;
|
|
56
|
-
} else if (arg === '--mode') {
|
|
57
|
-
stateMode = args[++i] as StateMode;
|
|
58
|
-
} else if (arg === '--out') {
|
|
59
|
-
outDir = args[++i];
|
|
60
|
-
} else if (arg === '--verbose') {
|
|
61
|
-
verbose = true;
|
|
62
|
-
} else if (!arg.startsWith('-')) {
|
|
63
|
-
paths.push(arg);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (paths.length === 0) {
|
|
68
|
-
console.error('Error: No paths specified');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { direction, paths, dryRun, stripAnnotations, stateMode, verbose, outDir };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function findFiles(paths: string[]): Promise<string[]> {
|
|
76
|
-
const files: string[] = [];
|
|
77
|
-
|
|
78
|
-
for (const p of paths) {
|
|
79
|
-
const resolved = resolve(p);
|
|
80
|
-
const matches = await glob('**/*.{tsx,jsx}', {
|
|
81
|
-
cwd: resolved,
|
|
82
|
-
absolute: true,
|
|
83
|
-
ignore: ['**/node_modules/**', '**/__tests__/**', '**/dist/**'],
|
|
84
|
-
});
|
|
85
|
-
files.push(...matches);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return files.sort();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function transformFile(
|
|
92
|
-
filePath: string,
|
|
93
|
-
options: CodemodOptions,
|
|
94
|
-
verbose: boolean,
|
|
95
|
-
): { code: string; changed: boolean } {
|
|
96
|
-
const source = readFileSync(filePath, 'utf-8');
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const result = transform(source, options);
|
|
100
|
-
const changed = result.code !== source;
|
|
101
|
-
|
|
102
|
-
if (verbose && changed) {
|
|
103
|
-
console.log(` [${options.direction}] ${relative(process.cwd(), filePath)}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { code: result.code, changed };
|
|
107
|
-
} catch (err: any) {
|
|
108
|
-
console.error(` [ERROR] ${relative(process.cwd(), filePath)}: ${err.message}`);
|
|
109
|
-
return { code: source, changed: false };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function main() {
|
|
114
|
-
const opts = parseArgs(process.argv);
|
|
115
|
-
const files = await findFiles(opts.paths);
|
|
116
|
-
|
|
117
|
-
console.log(`mm-codemod: ${opts.direction} — ${files.length} file(s)`);
|
|
118
|
-
|
|
119
|
-
if (opts.direction === 'verify') {
|
|
120
|
-
// Round-trip verification: forward(source) → reverse(result) → diff
|
|
121
|
-
let pass = 0;
|
|
122
|
-
let fail = 0;
|
|
123
|
-
|
|
124
|
-
for (const file of files) {
|
|
125
|
-
const source = readFileSync(file, 'utf-8');
|
|
126
|
-
const forward = transform(source, {
|
|
127
|
-
direction: 'forward',
|
|
128
|
-
annotate: true,
|
|
129
|
-
stateMode: opts.stateMode,
|
|
130
|
-
});
|
|
131
|
-
const roundTripped = transform(forward.code, {
|
|
132
|
-
direction: 'reverse',
|
|
133
|
-
annotate: false,
|
|
134
|
-
stateMode: opts.stateMode,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Normalize whitespace for comparison
|
|
138
|
-
const normalize = (s: string) => s.replace(/\s+/g, ' ').trim();
|
|
139
|
-
if (normalize(roundTripped.code) === normalize(source)) {
|
|
140
|
-
pass++;
|
|
141
|
-
if (opts.verbose) console.log(` [PASS] ${relative(process.cwd(), file)}`);
|
|
142
|
-
} else {
|
|
143
|
-
fail++;
|
|
144
|
-
console.log(` [FAIL] ${relative(process.cwd(), file)}`);
|
|
145
|
-
if (opts.verbose) {
|
|
146
|
-
console.log(' Expected (normalized):');
|
|
147
|
-
console.log(` ${normalize(source).slice(0, 200)}...`);
|
|
148
|
-
console.log(' Got (normalized):');
|
|
149
|
-
console.log(` ${normalize(roundTripped.code).slice(0, 200)}...`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
console.log(`\nResults: ${pass} pass, ${fail} fail out of ${files.length} files`);
|
|
155
|
-
process.exit(fail > 0 ? 1 : 0);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Forward or reverse
|
|
159
|
-
const codemodOptions: CodemodOptions = {
|
|
160
|
-
direction: opts.direction as 'forward' | 'reverse',
|
|
161
|
-
annotate: !opts.stripAnnotations,
|
|
162
|
-
stateMode: opts.stateMode,
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
let changed = 0;
|
|
166
|
-
let unchanged = 0;
|
|
167
|
-
let errors = 0;
|
|
168
|
-
|
|
169
|
-
for (const file of files) {
|
|
170
|
-
try {
|
|
171
|
-
const result = transformFile(file, codemodOptions, opts.verbose);
|
|
172
|
-
|
|
173
|
-
if (!result.changed) {
|
|
174
|
-
unchanged++;
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
changed++;
|
|
179
|
-
|
|
180
|
-
if (opts.dryRun) {
|
|
181
|
-
console.log(` [WOULD CHANGE] ${relative(process.cwd(), file)}`);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Determine output path
|
|
186
|
-
let outPath = file;
|
|
187
|
-
if (opts.outDir) {
|
|
188
|
-
const rel = relative(resolve(opts.paths[0]), file);
|
|
189
|
-
outPath = resolve(opts.outDir, rel);
|
|
190
|
-
const dir = dirname(outPath);
|
|
191
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
writeFileSync(outPath, result.code, 'utf-8');
|
|
195
|
-
} catch (err: any) {
|
|
196
|
-
errors++;
|
|
197
|
-
console.error(` [ERROR] ${relative(process.cwd(), file)}: ${err.message}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
console.log(`\nDone: ${changed} changed, ${unchanged} unchanged, ${errors} errors`);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
main().catch(err => {
|
|
205
|
-
console.error(err);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
});
|